Getting started
A checkout form asks the API what to draw for a country, then draws it. This page takes a form from nothing to the right fields for wherever the visitor is.
Loading a form
A cold form sends three requests at once. Only the first depends on the visitor: it says where they are and carries the rules for that country. The other two, the country list and every text in the page's language, are the same for everyone and the browser keeps them.
const base = 'https://i18n-rules.nextcommerce.com';
const [geo, countries, texts] = await Promise.all(
[
'/v1/geo?include=rules,states&lang=en',
'/v1/countries?lang=en',
'/v1/locales/en',
].map((path) => fetch(base + path).then((res) => res.json())),
);
const { address, fields } = geo.rules;Drawing the fields
address.layout is rows of field names, in the order the country writes an address. Draw one row per entry and one control per name; fields[name] says how.
for (const row of address.layout) {
for (const name of row) {
const field = fields[name];
const label = field.required ? field.label : field.label_optional;
// field.input.type: 'text', 'email', 'tel' or 'select'
// field.autocomplete: put it on the control, for browser autofill
}
}For a visitor in the US, city, state and ZIP Code share a row:
[["country"], ["first_name", "last_name"], ["line1"], ["line2"],
["city", "state", "postcode"], ["phone_number"]]Other countries order the rows differently; How countries differ shows how.
A field not in the layout is not asked for at all: not shown, not required, not checked, and not in fields. The email is in fields for every country but in no row; a form asks for it once. Values in address.fixed are not asked for and are submitted as they are.
Checking values
Each field carries its own checks and messages. A value is refused when it is empty and required, longer than input.max_length, holds an emoji, holds digits or symbols in a name, or, for a postcode or phone number, does not match format.pattern. Show the matching sentence from errors, which is already in the page's language.
A postcode is compared compact (upper case, no spaces or hyphens) and written back with format.masks. A phone number is shown with format.masks too, and submitted in E.164, which comes from the API rather than from the rule: see Checking a phone number. Whether a postcode is the chosen state's comes from the API too: see Checking a postcode. The Routes reference has both in detail.
Checking a phone number
The value an order submits for a phone number is E.164, +14155552671. Ask the API for it with what the shopper typed and the address country, when they pause typing or leave the field:
const res = await fetch(`${base}/v1/validate?lang=en`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ country: 'US', fields: { phone_number: '(415) 555-2671' } }),
});
const { fields } = await res.json();
// fields.phone_number: { valid: true, value: '+14155552671', country: 'US',
// type: 'fixed_line_or_mobile', national: '(415) 555-2671',
// international: '+1 415 555 2671' }Submit value when valid is true, and show national (or international, for a number from another country) once the shopper has left the field. country is the number's own, which need not be the address country: 011 44 7400 123456 typed on a US address is GB. A number that does not pass has no value; send it as typed, and let the order API read it. Refuse a number only by format.pattern, which is loose on purpose, never because this request failed or was slow: a shopper is not stopped by a problem that is ours.
Use fetch(), not navigator.sendBeacon, which ad blockers stop for every third-party host. The same request takes every field of a form at once; phone_number and postcode are checked, and any other field is answered { "valid": null }.
Checking a postcode
format.pattern tells whether a postcode is one the country uses. Whether it is one the chosen state uses is asked of the same route, with the state the form submits:
const res = await fetch(`${base}/v1/validate?lang=en`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ country: 'US', fields: { postcode: '10001', state: 'CA' } }),
});
const { fields } = await res.json();
// fields.postcode: { valid: false, state: 'NY', error: { code: 'not_in_state',
// message: 'Enter a valid ZIP Code for California' } }Show error.message under the field: it is a whole sentence in ?lang=, with the state named, and error.code says which check it failed. Send the page's language as ?lang=; without it the message follows the browser's language. state in the answer is the state the postcode is in, where only one state's postcodes start that way. A valid postcode comes back as its country writes it in value: 941031234 is 94103-1234.
Refuse a postcode by format.pattern without waiting for this request, and never because it failed or was slow. Whether to refuse not_in_state is the form's choice: a few real addresses sit across a state line from their postcode (Texhoma, Texas has an Oklahoma ZIP code), so a form that would rather not refuse them shows the message and lets the order through. Each country's page under Countries lists how its states' postcodes start.
Changing country
When the shopper picks another country, fetch its rules and redraw, keeping what they have typed in fields that are still there. Picking the US:
const rules = await fetch(`${base}/v1/countries/US?lang=en&include=states`).then((res) =>
res.json(),
);?country=US with /v1/geo?include=rules does the same in the first request, and ?countries= limits both the list and the country a form opens on to where a store ships.
Writing an address
To show an address an order already holds, on an order page or a receipt, use the same rules: address.format says how the country writes one.
const rules = await fetch(
`${base}/v1/countries/${order.country}?lang=en&include=states`,
).then((res) => res.json());Use rules.address.format_latin when the address is in Latin script and the country has one, and rules.address.format otherwise. Write the state by its name: find the stored state code in rules.states, and use the stored value only where no option has it. Then write each line from its parts, a field's value or the text between fields, leaving out an empty field and the text beside it. Routes has the rule in full, and rules.address.example is a real address to try it on.
Next steps
- Browse a country's form and rules under Countries.
- Read what differs between countries in How countries differ.
- Look up a field or response in the reference: Fields, CountryResponse, GeoResponse, ValidateResponse, PhoneNumberResult, PostcodeResult.