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
13 changes: 13 additions & 0 deletions country-explorer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 63 additions & 12 deletions country-explorer/src/api/countries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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.
// Thrown for a failed request: a non-2xx response (status holds the real
// HTTP status) or a network-level failure (status 0).
export class ApiError extends Error {
status: number;

Expand All @@ -14,31 +12,84 @@ export class ApiError extends Error {
}
}

async function handleResponse<T>(response: Response): Promise<T> {
// The internal shape this module returns to the rest of the app. This is a
// placeholder until #8 ("Define TypeScript Types for Country Data") lands
// its own Country type in src/types/country.ts — once it does, delete this
// and the RawCountry type below, import that one instead, and point
// `toCountry` at it. Kept local to this file (instead of touching
// src/types/country.ts) so the two issues can be worked on in parallel
// without both branches editing the same file.
export interface Country {
name: string;
capital: string;
region: string;
subregion: string;
population: number;
area: number;
flagUrl: string;
currencies: string[];
languages: string[];
}

// The relevant slice of the raw shape returned by https://countries.dev.
interface RawCountry {
name: string;
capital: string;
region: string;
subregion: string;
population: number;
area: number;
flags: { svg: string; png: string };
currencies: { code: string; name: string; symbol: string }[];
languages: { name: string }[];
}

function toCountry(raw: RawCountry): Country {
return {
name: raw.name,
capital: raw.capital,
region: raw.region,
subregion: raw.subregion,
population: raw.population,
area: raw.area,
flagUrl: raw.flags.svg,
currencies: raw.currencies.map((currency) => `${currency.name} (${currency.code})`),
languages: raw.languages.map((language) => language.name),
};
}

async function fetchJson<T>(url: string): Promise<T> {
let response: Response;
try {
response = await fetch(url);
} catch {
throw new ApiError(0, 'Network request failed. Check your internet connection.');
}

if (!response.ok) {
let message = response.statusText;
let message = response.statusText || `Request failed with status ${response.status}`;
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.
// Response body wasn't JSON; fall back to the status text above.
}
throw new ApiError(response.status, message);
}

return response.json() as Promise<T>;
}

// GET /region/{region} — all countries in a region, e.g. "europe".
// GET /region/{region} — every country 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);
const raw = await fetchJson<RawCountry[]>(`${BASE_URL}/region/${encodeURIComponent(region)}`);
return raw.map(toCountry);
}

// 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);
const raw = await fetchJson<RawCountry>(`${BASE_URL}/alpha/${encodeURIComponent(code)}`);
return toCountry(raw);
}