Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion country-explorer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
100 changes: 100 additions & 0 deletions country-explorer/src/api/README.md
Original file line number Diff line number Diff line change
@@ -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`).
44 changes: 44 additions & 0 deletions country-explorer/src/api/countries.ts
Original file line number Diff line number Diff line change
@@ -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<T>(response: Response): Promise<T> {
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<T>;
}

// GET /region/{region} — all countries in a region, e.g. "europe".
export async function getCountriesByRegion(region: string): Promise<Country[]> {
const response = await fetch(`${BASE_URL}/region/${encodeURIComponent(region)}`);
return handleResponse<Country[]>(response);
}

// GET /alpha/{code} — a single country by ISO alpha-2 or alpha-3 code.
export async function getCountryByAlpha(code: string): Promise<Country> {
const response = await fetch(`${BASE_URL}/alpha/${encodeURIComponent(code)}`);
return handleResponse<Country>(response);
}
48 changes: 48 additions & 0 deletions country-explorer/src/types/country.ts
Original file line number Diff line number Diff line change
@@ -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;
}