Countries

A complete rebuild of the REST Countries challenge into a production-ready React application. The project combines normalized API data, URL-driven search and pagination, resilient routing, persistent themes, accessible interactions and careful performance work across mobile and desktop.

Adrian Nasrat

· Visit Project

Explore the finished application and its source code:

From frontend challenge to production application

Countries began as a Frontend Mentor challenge: fetch country data, provide a search field, filter by region and display a detailed country view. I used the brief as a starting point, then rebuilt the project as a complete product rather than treating it as a static implementation exercise.

The finished application lets users browse 250 countries, combine search and regional filtering, move through paginated results and navigate directly between bordering countries. Search, region and page values are stored in the URL, so a filtered view can be refreshed, bookmarked or shared without losing its state.

Countries application showing the country grid in light mode and Belgium details in dark mode
The application combines a searchable country overview with detailed country pages and persistent light and dark themes.

The product goals

The central challenge was not simply displaying API data. The experience also needed to remain predictable as users searched, changed pages, opened a country and returned to the result they had been viewing.

I defined five goals for the rebuild:

  • Preserve context: filters, pagination and scroll position should survive navigation to a country and back.
  • Design mobile first: cards, controls and detail views should reflow cleanly without compressed layouts or horizontal scrolling.
  • Treat accessibility as architecture: semantics, focus management, live announcements and reduced motion should be part of the implementation.
  • Handle external data defensively: incomplete records and failed flag images should never break the interface.
  • Ship a production build: routing, metadata, caching, error states and loading behavior should work outside the local development environment.

A stable data and routing architecture

Country data is requested through a dedicated API layer and normalized before it reaches the interface. This gives the components a stable internal model even when the external response contains missing or differently shaped values. The same layer caches successful results and deduplicates concurrent requests, which avoids repeated network work when moving between the overview and detail pages.

let countriesCache = null;
let countriesRequest = null;

export async function fetchCountries({ force = false } = {}) {
  if (!force && countriesCache) return countriesCache;
  if (!force && countriesRequest) return countriesRequest;

  const request = fetch(COUNTRIES_API_URL).then(async (response) => {
    if (!response.ok) {
      throw new Error("Failed to fetch countries");
    }

    const countries = await response.json();
    return countries.map(normalizeCountry);
  });

  countriesRequest = request;

  try {
    countriesCache = await request;
    return countriesCache;
  } finally {
    if (countriesRequest === request) {
      countriesRequest = null;
    }
  }
}

React Router drives both page navigation and the state of the country list. A URL such as /?search=sw&region=Europe&page=2 fully describes the current view. Pagination changes intentionally return the user to the filters, while browser Back navigation restores the exact scroll position they left behind. Direct routes such as /country/SWE also work after a refresh through the production Vercel rewrite configuration.

const [searchParams, setSearchParams] = useSearchParams();
const searchQuery = searchParams.get("search") ?? "";

function handleSearchChange(value) {
  setSearchParams(
    (currentParams) => {
      const nextParams = new URLSearchParams(currentParams);

      if (value) {
        nextParams.set("search", value);
      } else {
        nextParams.delete("search");
      }

      return nextParams;
    },
    { replace: true },
  );
}

Designing for every viewport

The responsive system uses one shared page container for the navigation, filter controls, country grid, loading states and country details. This keeps every screen aligned and prevents individual pages from developing slightly different widths or padding rules.

The country grid progresses deliberately from one to four columns rather than using unpredictable automatic sizing. Flag areas use controlled aspect ratios, cards maintain consistent proportions and the detail layout only becomes two-column when there is enough room for both the flag and information panel.

Responsive design exploration for the Belgium country details page across desktop, tablet and mobile layouts
Early responsive exploration established how country details, navigation and border links should adapt across breakpoints.

Theme without compromise

The theme toggle supports explicit light and dark preferences while respecting the operating system when the user has not made a choice. An explicit selection is persisted in local storage and applied to the document before React renders, preventing a flash of the wrong theme during reloads.

Every elevated surface, control, skeleton, error message and focus indicator was reviewed in both themes. The result is one coherent interface rather than a dark palette applied only to the main screens.

Mobile Belgium details page displayed side by side in dark and light themes
Light and dark themes preserve the same hierarchy, readability and responsive behavior.

Accessibility built into the interaction model

The application was audited as a keyboard, screen-reader and reduced-motion experience and not only as a visual interface. The implementation includes:

  • semantic headings, forms, lists and named navigation landmarks;
  • a skip link and managed focus after route navigation;
  • visible focus states for every interactive control;
  • live announcements for result counts and empty states;
  • descriptive flag alternatives and accessible pagination states;
  • animations that respect prefers-reduced-motion; and
  • complete mobile reflow without horizontal scrolling at 320 pixels.

During the production audit, Lighthouse scored the deployed application 100 for accessibility, 100 for best practices and 100 for SEO.

Performance and resilience

The Home, Country Details and Not Found routes are loaded as separate bundles. Above-the-fold flags receive higher loading priority, later images are lazy loaded and decoded asynchronously, and preconnections reduce the setup cost of external requests. Flag rendering also falls through multiple image sources and finally to a readable fallback instead of exposing a broken image.

Loading skeletons mirror the final grid to reduce layout movement. Failed API requests produce a focused error state with retry support, missing country values receive clear fallbacks and unknown URLs render a dedicated 404 page.

The production Lighthouse audit reached a 93 performance score, with zero total blocking time and zero cumulative layout shift in the measured run.

Technology and responsibilities

  • React 19 for component composition and application state
  • React Router 6 for data routing, URL state and navigation restoration
  • Vite 8 for development and optimized production builds
  • Tailwind CSS 4 for the responsive design system and theme variants
  • countries.dev for country information and flag sources
  • Vercel for deployment, SPA rewrites, caching and response headers

I owned the complete implementation: interface architecture, responsive design, API normalization, navigation behavior, accessibility, animation, performance work, deployment configuration and production verification.

Outcome

What started as a compact API challenge became an exercise in building a mature frontend experience. The most valuable outcome was learning to treat seemingly small behaviors such as restoring scroll position, preserving filters, announcing result changes and handling failed images as core product requirements.

The result is a responsive and resilient country explorer that remains easy to use across devices, input methods, themes and navigation paths.

Contact

Questions or need more details? Ping me on Discord, or any of my other social media links.