diff --git a/country-explorer/README.md b/country-explorer/README.md index ebcc408..089a035 100644 --- a/country-explorer/README.md +++ b/country-explorer/README.md @@ -16,7 +16,7 @@ src/ └── main.tsx # Application entry point ``` -`api/`, `hooks/`, `types/`, and `utils/` are created once there is real code to put in them. +`api/`, `hooks/`, `types/`, and `utils/` are created once there is real code to put in them. See [`src/api/README.md`](./src/api/README.md) for which REST API is used, the selected countries, data fields, and error handling. Naming conventions: diff --git a/country-explorer/src/api/README.md b/country-explorer/src/api/README.md new file mode 100644 index 0000000..a084bc0 --- /dev/null +++ b/country-explorer/src/api/README.md @@ -0,0 +1,100 @@ +# REST API + +The application uses [countries.dev](https://countries.dev) to fetch country data. It's a free, publicly available API with no API key, sign-up, or authentication required. It's the successor to `apicountries.com`, which now redirects to it, and returns the same field shape as the (now retired) `restcountries.com` v2 API. + +## Endpoints used + +- `GET /region/{region}` — fetches every country in a region. Used once to load all European countries: `/region/europe`. +- `GET /alpha/{code}` — fetches a single country by its ISO alpha-2/alpha-3 code. Used when looking up or refreshing one specific country by code. + +## Selected countries + +All 53 entries returned by `/region/europe`: + +| Country | Alpha-2 | Alpha-3 | +| ---------------------------------------------------- | ------- | ------- | +| Åland Islands | AX | ALA | +| Albania | AL | ALB | +| Andorra | AD | AND | +| Austria | AT | AUT | +| Belarus | BY | BLR | +| Belgium | BE | BEL | +| Bosnia and Herzegovina | BA | BIH | +| Bulgaria | BG | BGR | +| Croatia | HR | HRV | +| Cyprus | CY | CYP | +| Czech Republic | CZ | CZE | +| Denmark | DK | DNK | +| Estonia | EE | EST | +| Faroe Islands | FO | FRO | +| Finland | FI | FIN | +| France | FR | FRA | +| Germany | DE | DEU | +| Gibraltar | GI | GIB | +| Greece | GR | GRC | +| Guernsey | GG | GGY | +| Vatican City | VA | VAT | +| Hungary | HU | HUN | +| Iceland | IS | ISL | +| Ireland | IE | IRL | +| Isle of Man | IM | IMN | +| Italy | IT | ITA | +| Jersey | JE | JEY | +| Latvia | LV | LVA | +| Liechtenstein | LI | LIE | +| Lithuania | LT | LTU | +| Luxembourg | LU | LUX | +| North Macedonia | MK | MKD | +| Malta | MT | MLT | +| Moldova (Republic of) | MD | MDA | +| Monaco | MC | MCO | +| Montenegro | ME | MNE | +| Netherlands | NL | NLD | +| Norway | NO | NOR | +| Poland | PL | POL | +| Portugal | PT | PRT | +| Republic of Kosovo | XK | UNK | +| Romania | RO | ROU | +| Russian Federation | RU | RUS | +| San Marino | SM | SMR | +| Serbia | RS | SRB | +| Slovakia | SK | SVK | +| Slovenia | SI | SVN | +| Spain | ES | ESP | +| Svalbard and Jan Mayen | SJ | SJM | +| Sweden | SE | SWE | +| Switzerland | CH | CHE | +| Ukraine | UA | UKR | +| United Kingdom of Great Britain and Northern Ireland | GB | GBR | + +This list includes a handful of dependent territories alongside sovereign states (Åland Islands, Faroe Islands, Gibraltar, Guernsey, Isle of Man, Jersey, Svalbard and Jan Mayen), following the API's own region classification. Each country's `independent` field can be used later to filter down to sovereign states only, if that turns out to be desired. + +## Data fields used + +| Field (from the wireframe) | API key(s) | Notes | +| -------------------------- | -------------------------- | --------------------------------------------------------------------------------------- | +| Name | `name` | | +| Flag | `flags.svg`, `flags.png` | `flag` also gives an emoji flag | +| Capital | `capital` | | +| Region | `region`, `subregion` | Used by `RegionFilter` | +| Population | `population` | | +| Area | `area` | In km² | +| Currency | `currencies` | | +| Languages | `languages` | | +| — | `alpha2Code`, `alpha3Code` | Not shown in the UI; used internally as a stable id/key and for `/alpha/{code}` lookups | + +## Error handling + +The API responds with a normal HTTP status code plus a small JSON error body: + +| Status | Meaning | Example body | +| ------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `400` | Invalid parameter (e.g. a non-numeric value where a numeric code is expected) | `{ "error": "..." }` | +| `404` | No match — unknown/typo'd code or name | `{ "error": "Country not found" }` | +| `429` | Abuse ceiling hit (~60 requests/second per IP; there's no API key or per-key quota) | `{ "error": "Too Many Requests", "message": "Rate limit reached" }` | + +A `404` is treated as "no result" in the UI (e.g. an empty/error state), not as a crash, since it normally just means the requested code or name didn't match anything. The app only needs one `/region/europe` call plus occasional `/alpha/{code}` lookups, so the `429` ceiling should never realistically be hit in normal use. + +## Data storage + +Fetched country data is only kept in memory (component state / the data-fetching library's cache) and is never written to `localStorage` or `sessionStorage`. Only user-created state — favorites, selected region, sort option — may be persisted to Web Storage (see `wireframes-and-component-structure.md`). diff --git a/country-explorer/src/api/countries.ts b/country-explorer/src/api/countries.ts new file mode 100644 index 0000000..ab2c116 --- /dev/null +++ b/country-explorer/src/api/countries.ts @@ -0,0 +1,44 @@ +import type { Country } from '../types/country'; + +const BASE_URL = 'https://countries.dev'; + +// Thrown for any non-2xx response. `status` lets callers distinguish a +// "no result" 404 (show an empty state) from a real failure. +export class ApiError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.name = 'ApiError'; + this.status = status; + } +} + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + let message = response.statusText; + try { + const body: unknown = await response.json(); + if (body && typeof body === 'object' && 'error' in body) { + message = String((body as { error: unknown }).error); + } + } catch { + // Response body wasn't JSON; fall back to statusText. + } + throw new ApiError(response.status, message); + } + + return response.json() as Promise; +} + +// GET /region/{region} — all countries in a region, e.g. "europe". +export async function getCountriesByRegion(region: string): Promise { + const response = await fetch(`${BASE_URL}/region/${encodeURIComponent(region)}`); + return handleResponse(response); +} + +// GET /alpha/{code} — a single country by ISO alpha-2 or alpha-3 code. +export async function getCountryByAlpha(code: string): Promise { + const response = await fetch(`${BASE_URL}/alpha/${encodeURIComponent(code)}`); + return handleResponse(response); +} diff --git a/country-explorer/src/types/country.ts b/country-explorer/src/types/country.ts new file mode 100644 index 0000000..2bd537b --- /dev/null +++ b/country-explorer/src/types/country.ts @@ -0,0 +1,48 @@ +export interface Currency { + code: string; + name: string; + symbol: string; +} + +export interface Language { + name: string; + nativeName: string; + iso639_1: string; + iso639_2: string; +} + +export interface Flags { + png: string; + svg: string; +} + +// Shape returned by https://countries.dev for both `/region/{region}` (as an +// array) and `/alpha/{code}` (as a single object). A few fields are missing +// on some entries (e.g. dependent territories don't have `cioc`/`gini`, and +// some don't have `borders`), so they're marked optional here. +export interface Country { + name: string; + nativeName: string; + alpha2Code: string; + alpha3Code: string; + numericCode: string; + capital: string; + region: string; + subregion: string; + population: number; + populationDensity: number; + area: number; + currencies: Currency[]; + languages: Language[]; + timezones: string[]; + latlng: [number, number]; + demonym: string; + flag: string; + flags: Flags; + independent: boolean; + callingCodes: string[]; + topLevelDomain: string[]; + borders?: string[]; + cioc?: string; + gini?: number; +}