Geo World is a country explorer built with Nuxt 4. It brings together country facts, saved favorites, and live weather data so each country page feels more useful than a static encyclopedia entry.
The project was also a way for me to properly step into the Vue and Nuxt ecosystem: file-based routing, Nuxt layers, composables, server routes, runtime config, Pinia, Tailwind, PrimeVue, and SSR-friendly UI behavior.
Most country apps stop at flags, capitals, and population. Geo World goes a little further by connecting that country data with real weather context.
You can browse countries, filter by region, search by country or capital, save favorites, open a detailed country profile, and see current weather, hourly forecasts, daily forecasts, alerts, local time, languages, borders, maps, currencies, area, population, and density in one place.
Create a .env file in the project root:
NUXT_PUBLIC_COUNTRIES_API_BASE=https://api.restcountries.com/countries/v5
NUXT_REST_COUNTRIES_API_KEY=your_restcountries_api_key
NUXT_PUBLIC_OPENWEATHER_API_URL_BASE=https://api.openweathermap.org/data/3.0
NUXT_PUBLIC_OPENWEATHER_API_ICON=https://openweathermap.org/img/wn
NUXT_OPENWEATHER_API_KEY_BASE=your_openweather_api_keyNUXT_REST_COUNTRIES_API_KEY and NUXT_OPENWEATHER_API_KEY_BASE are private server runtime config. They are used by the Nitro country and weather routes and are not exposed to the client. Get a free REST Countries v5 key at restcountries.com/docs (500 requests/month, no card required).
Install dependencies:
npm installRun the development server:
npm run devOpen:
http://localhost:3000Build and run production locally:
npm run build
npm run startUseful checks:
npm run lint
npm run format:check- Browse countries from REST Countries v5, fetched and cached server-side to stay within the free API quota.
- Search by country name or capital.
- Filter countries by region.
- Load countries incrementally with client-side pagination.
- Open detailed country pages with flags, facts, maps, languages, currencies, borders, and formatted statistics.
- View current weather, hourly forecast, daily forecast, weather alerts, and local time from OpenWeather.
- Save favorite countries with persisted Pinia state.
- Switch between dark and light themes with
@nuxtjs/color-mode. - Use PrimeVue components with the Aura theme.
- Add small UI transitions with
@vueuse/motion. - Keep external API calls behind Nuxt server routes.
- Convert third-party API responses into stable UI models through shared mappers.
| Area | Tools |
|---|---|
| Framework | Nuxt 4, Vue 3, Vue Router |
| Language | TypeScript |
| State | Pinia, pinia-plugin-persistedstate |
| Styling | Tailwind CSS, PrimeVue, Aura preset |
| Theme | @nuxtjs/color-mode |
| Icons | @nuxt/icon, Iconify Lucide set |
| Motion | @vueuse/motion |
| Dates | date-fns, date-fns-tz, dayjs |
| API Layer | Nuxt Nitro server routes, ofetch |
| Quality | ESLint 9, Nuxt ESLint, Prettier, simple-import-sort, unused-imports |
Geo World uses Nuxt layers to keep the app split by domain. The root app owns the shell, layout, global state, shared utilities, and server routes. The country and weather features live in their own layers.
geo_world/
|-- app/
| |-- assets/css/ Global Tailwind layers and reusable utility classes
| |-- components/ App shell components
| |-- composables/ App-level composables
| |-- layouts/ Default layout
| |-- pages/ Root app pages, including favorites
| |-- plugins/ Client plugins
| `-- stores/ Pinia stores
|-- layers/
| |-- countries/ Country explorer layer
| | `-- app/
| | |-- components/ Country cards, search, tabs, detail sections
| | |-- composables/ Country fetching, filtering, pagination
| | `-- pages/ /countries and /countries/[code]
| `-- weather/ Weather layer
| `-- app/
| |-- components/ Current, hourly, daily, alerts, local time
| `-- composables/ Weather fetching and clock updates
|-- server/
| |-- api/ Nitro routes that proxy external APIs
| `-- utils/
| `-- restcountries/ Paginated fetch + cache + REST Countries v5 -> DTO adapter
|-- shared/
| |-- mappers/ DTO-to-UI transformation layer
| |-- types/ External DTO and internal UI types
| |-- ui/motion/ Motion tokens, presets, and config
| `-- utils/ Formatters, error wrapper, class merging
|-- public/ Static assets
|-- nuxt.config.ts Main Nuxt configuration
|-- tailwind.config.js Tailwind theme configuration
`-- eslint.config.mjs ESLint workspace rules/countriescallsuseGetCountries().useGetCountries()calls the internal route/api/countries.server/api/countries/index.get.tsreads the cached, paginated REST Countries v5 dataset (getCachedCountries()), filters out entries with no ISO alpha-2 code, and adapts each one (mapV5ToCountryDTO()).mapCountryDtoToUI()converts the adapted DTO intoCountryUI.useCountriesFilter()handles search and region filtering.useClientSidePagination()controls how many cards are visible.CountryCardrenders each item and sends favorite changes to Pinia.
/countries/[code]reads the country code from the route.useGetCountryDetails(code)calls/api/countries/[code].- The server route looks up the country in the same cached dataset and adapts it (
mapV5ToCountryDetailDTO()), returning a 404 if the code doesn't match any country. - If the country has borders, the route resolves the neighboring countries from the same cached dataset (no extra API call).
mapCountryDetailsDtoToUi()formats the result for the UI.- The page extracts latitude and longitude from the country data.
- Weather loading starts only after both coordinates are available.
useGetWeatherData(lat, lon)creates a weather request withimmediate: false.- A watcher waits until both coordinates are present.
- The composable executes
/api/weather. server/api/weather/index.get.tscalls OpenWeather One Call.mapWeatherDtoToUI()formats current, hourly, daily, and alert data.- Weather components receive display-ready values.
Favorites are stored as country codes in a Pinia setup store:
favorites: string[]The store exposes:
isFavorite(code)toggleFavorite(code)
Persistence is handled by pinia-plugin-persistedstate, so saved countries remain after reload.
Returns a normalized list of countries, sourced from a server-side cache (see below).
Returned shape:
CountryUI[]Returns one normalized country details payload, including border countries when available. Returns a 404 if the code doesn't match any country.
Returned shape:
CountryDetailUI{NUXT_PUBLIC_COUNTRIES_API_BASE} (REST Countries v5) caps each request at 100 countries on the free tier, and the free tier is limited to 500 requests/month. server/utils/restcountries/client.ts paginates through the full ~250-country set (offset/limit/meta.more) and caches the result for 24 hours via defineCachedFunction, so every visitor is served from the same cached list instead of hitting the upstream API directly. This keeps usage to roughly 3 requests/day regardless of traffic.
server/utils/restcountries/adapter.ts converts v5's response shape (e.g. names.common, codes.alpha_2, economy.gini_coefficient) back into the CountryDTO / CountryDetailDTO shapes the rest of the app already expects, so the mappers below didn't need to change. One known gap: v5 has no coatOfArms field, so that UI section never renders.
Returns normalized weather data for a coordinate pair.
External source:
{NUXT_PUBLIC_OPENWEATHER_API_URL_BASE}/onecallQuery parameters sent to OpenWeather:
lat
lon
exclude=minutely
units=metric
appid={NUXT_OPENWEATHER_API_KEY_BASE}Returned shape:
WeatherUIThe app separates external API DTOs from UI-facing models.
REST Countries DTO -> country mapper -> CountryUI / CountryDetailUI
OpenWeather DTO -> weather mapper -> WeatherUIThat keeps Vue components away from third-party response details and gives the UI formatted values for population, area, dates, time, weather icons, and border links.
The interface is built with Tailwind utility classes, PrimeVue components, the Aura preset, Lucide icons, and shared motion presets.
Dark mode uses Tailwind's class strategy and @nuxtjs/color-mode. PrimeVue follows the same .dark selector, so custom UI and PrimeVue components stay aligned.
Server API calls are wrapped by fetchFromExternalApi().
The wrapper converts external fetch failures and runtime errors into Nuxt createError() responses. Client requests go through useApiClient(), which wraps useFetch() and shows PrimeVue toast messages when requests fail.
nuxt.config.tspoints Tailwind totailwind.config.ts, while the repository currently containstailwind.config.js.- Some labels contain mojibake characters where temperature and square-kilometer units should be displayed.
- The project has linting and formatting scripts, but no test runner is configured yet.
- A
.env.examplefile would make setup clearer. - Real screenshots would make the README much easier to scan.
No license file is currently present.
