Skip to content

Latest commit

Β 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌦️ Weather API Aggregator

A Spring Boot backend API that aggregates weather data from multiple external sources, with automatic fallback, a circuit breaker, JWT authentication and normalized error handling β€” designed to keep working even when an external source fails.

Clients built on this API: Web (Next.js) (live) Β· iOS (Swift/SwiftUI) Β· Android (Kotlin/Compose) β€” none of them talk to Open-Meteo/OpenWeatherMap directly, every request goes through this API.

Live API: weather-api-production-68ff.up.railway.app (Swagger UI is disabled on this deployment β€” see Notes; run locally to explore it interactively)

Weather API Aggregator queries a primary weather provider (Open-Meteo) and falls back automatically to a secondary one (OpenWeatherMap) if the first is down, each call protected by a Resilience4j circuit breaker and retry with exponential backoff. On top of that sits a full per-user layer β€” JWT authentication with refresh tokens, search history, favorite cities and unit preferences backed by PostgreSQL β€” plus an in-memory cache, per-user rate limiting and role-based access (regular users vs. admins) for aggregate stats and user management.

πŸ“¦ What's Inside

  • πŸ”Ž Current weather lookup by city, with unit normalization (Celsius/km-h or Fahrenheit/mph)
  • πŸ“ˆ Hourly and daily forecast lookup by city (Open-Meteo), cached the same way as current weather
  • πŸ”€ City search/autocomplete endpoint (Open-Meteo geocoding), for typeahead search boxes in the clients
  • 🧩 Providers decoupled behind a Strategy/Adapter interface β€” swapping or adding a provider never touches the controller or the API contract
  • πŸ” Automatic fallback between providers (Open-Meteo β†’ OpenWeatherMap): if the primary fails, the request is served by the secondary one transparently
  • ⚑ Circuit breaker + retry with exponential backoff (Resilience4j) per provider β€” a provider that's systematically failing stops being called for a few seconds instead of piling up load, and transient errors are retried before giving up on that provider
  • 🌑️ Unit normalization across providers β€” OpenWeatherMap natively returns Kelvin and m/s; the conversion to Celsius/Fahrenheit and km/h/mph happens in the application, never on the provider's side
  • ⚑ In-memory cache (Caffeine), configurable TTL, with an explicit fromCache flag showing whether a response came from cache
  • πŸ“Š Aggregate stats endpoint (admin only) β€” total users, searches, favorites, the most-searched city and live cache hit/miss counts
  • πŸ” JWT authentication (register/login) with refresh token rotation and logout/revocation, passwords hashed with BCrypt
  • πŸ‘€ Role-based access (USER/ADMIN) β€” admin-only endpoints for aggregate stats and user management, gated at the Spring Security config level
  • πŸ•˜ Per-user search history and favorite cities, addable and removable (PostgreSQL, Flyway migrations)
  • βš™οΈ Per-user unit preference β€” omitting units on a search falls back to the saved preference
  • πŸ“ GPS-based lookup β€” current weather for the caller's coordinates, reverse-geocoded to a city
  • 🌊 Marine conditions (water temperature, wave height/direction/period) and derived insights (moon phase, UV risk, outdoor-activity and fishing-condition scores) for coastal/any cities
  • 🚧 Rate limiting (Bucket4j), configurable per bucket type β€” per authenticated user, and per IP for both /auth/** and any other unauthenticated request β€” with a normalized 429 response
  • 🚦 Normalized errors that never leak the raw external provider error: 404 city not found, 502 provider unavailable, 429 quota/rate limit exceeded, 400 invalid input, 401 unauthenticated, 409 conflict (duplicate email/favorite)
  • πŸ—ΊοΈ Weather descriptions translated from Open-Meteo's WMO weather codes (OpenWeatherMap already returns its own description)
  • πŸ“‘ Interactive API documentation via Swagger/OpenAPI
  • βœ… Unit, integration (WireMock + a real PostgreSQL instance) and end-to-end tests β€” including one that forces a real circuit breaker trip β€” at ~97% line coverage

πŸ› οΈ Tech Stack

Java Spring Boot Spring Security PostgreSQL Flyway JWT Resilience4j Maven Caffeine Bucket4j JUnit5 WireMock OpenAPI

πŸ—οΈ Architecture

weather-api/
β”œβ”€β”€ src/main/java/com/vidi/weather/
β”‚   β”œβ”€β”€ controller/            # Weather (+ forecast/marine/insights/nearby/history/favorites), User, Auth, Admin, Stats
β”‚   β”œβ”€β”€ service/                 # cache/provider/fallback orchestration, resilience, users, history, favorites, admin
β”‚   β”œβ”€β”€ provider/                # Strategy/Adapter interface + Open-Meteo + OpenWeatherMap
β”‚   β”œβ”€β”€ security/                # JWT, refresh tokens, filters, rate limiting, roles, UserDetails
β”‚   β”œβ”€β”€ entity/                  # JPA entities (User with Role, SearchHistoryEntry, Favorite, RefreshToken)
β”‚   β”œβ”€β”€ repository/              # Spring Data JPA
β”‚   β”œβ”€β”€ model/                   # internal domain (immutable)
β”‚   β”œβ”€β”€ dto/                     # API contract (responses, errors, auth, preferences)
β”‚   β”œβ”€β”€ config/                  # cache, RestTemplate, security, rate limit, properties
β”‚   β”œβ”€β”€ exception/                # domain exceptions + global handler
β”‚   └── util/                    # weather code mapping + unit conversion
β”œβ”€β”€ src/main/resources/db/migration/  # Flyway migrations
β”œβ”€β”€ src/test/java/                # unit, repository (real Postgres), WireMock, MockMvc, security, fallback/circuit breaker tests
β”œβ”€β”€ LICENSE
└── pom.xml

Why these choices

  • Strategy/Adapter for providers: WeatherProvider is the only contract the rest of the app knows about. Open-Meteo and OpenWeatherMap each normalize their own response shape into the same WeatherData, so adding a third provider later is additive, not a rewrite.
  • Caffeine over Redis: for a single-instance API, an in-memory cache is enough and avoids standing up extra infrastructure. Redis is the natural next step once the app runs on more than one instance and needs a shared cache.
  • Open-Meteo as the primary provider: free, no API key required, so the project runs out of the box with zero setup friction. OpenWeatherMap is the secondary/fallback provider, which does need a free API key (see How to Run).
  • Resilience4j circuit breaker + retry, not a hand-rolled fallback loop: each provider gets its own breaker and retry policy configured declaratively in application.yml, so a systematically failing provider is skipped instead of retried forever, while transient errors (a single dropped request) still get absorbed before falling back.
  • PostgreSQL + Flyway over JPA auto-DDL: ddl-auto: validate plus a versioned migration means the schema is explicit and reviewable, not implicitly inferred from entity annotations.
  • Stateless JWT over sessions: no server-side session store to scale, and CSRF protection is correctly disabled for this reason β€” it protects cookie-based sessions, which this API doesn't use.
  • Immutable entities: JPA entities have no public setters; updates (e.g. changing a user's preferred units) go through a withX copy method and repository.save(...), keeping the "never mutate in place" rule even inside Hibernate-managed objects.

🌐 API

POST /api/v1/auth/register                 β€” register (returns a JWT + refresh token)
POST /api/v1/auth/login                    β€” log in (returns a JWT + refresh token)
POST /api/v1/auth/refresh                  β€” exchange a refresh token for a new access + refresh token pair
POST /api/v1/auth/logout                   β€” revoke a refresh token

GET  /api/v1/weather?city=&units=          β€” current weather, with automatic fallback (authenticated)
GET  /api/v1/weather/nearby?lat=&lon=      β€” current weather for the caller's GPS coordinates, reverse-geocoded to a city
GET  /api/v1/weather/forecast?city=&units= β€” hourly + daily forecast (Open-Meteo only, cached)
GET  /api/v1/weather/marine?city=&units=   β€” sea conditions (water temp, wave height/direction/period) for a coastal city
GET  /api/v1/weather/insights?city=&units= β€” derived insights: moon phase, UV risk, outdoor-activity score, fishing conditions
GET  /api/v1/weather/history               β€” search history
DELETE /api/v1/weather/history/{id}        β€” remove a single search history entry
DELETE /api/v1/weather/history             β€” clear the caller's entire search history
GET  /api/v1/weather/favorites             β€” list favorites
POST /api/v1/weather/favorites             β€” add a favorite
DELETE /api/v1/weather/favorites?city=     β€” remove a favorite

GET  /api/v1/geocoding?query=&limit=       β€” city search/autocomplete (Open-Meteo geocoding, cached)

GET  /api/v1/user/me                       β€” the authenticated user's profile, including role
GET  /api/v1/user/preferences              β€” get preferences
POST /api/v1/user/preferences              β€” update preferences

GET  /api/v1/admin/users                   β€” list every registered user (admin only)
DELETE /api/v1/admin/users/{id}            β€” delete a user account (admin only)

GET  /api/v1/stats                         β€” aggregate usage stats (users, searches, favorites, cache hit rate) (admin only)
Scenario Status
Success 200 / 201
Success, no response body (logout, delete favorite/user) 204
Unauthenticated 401
Insufficient role (non-admin hitting an admin-only endpoint) 403
City / favorite / user not found 404
Conflict (duplicate email/favorite) 409
External provider unavailable 502
Provider quota or rate limit exceeded 429
Invalid or missing parameter 400

πŸš€ How to Run

Prerequisites: Java 21, Maven and a local PostgreSQL instance.

# 1. Clone the repository
git clone https://github.com/VidiPT89/WeatherAPI.git
cd WeatherAPI

# 2. Make sure `java`/`mvn` resolve to Java 21
#    (skip this if `java -version` already reports 21; on macOS with Homebrew,
#    versioned JDKs are installed keg-only and aren't on PATH by default)
export JAVA_HOME="/opt/homebrew/opt/openjdk@21"
export PATH="$JAVA_HOME/bin:$PATH"

# 3. Create the database (one time)
createuser weather_api --pwprompt
createdb weather_api -O weather_api

# 4. Run the application (Flyway applies migrations automatically)
mvn spring-boot:run

The database connection, JWT secret/expiration, rate limits and the OpenWeatherMap API key are all configurable via environment variables (DB_URL, DB_USERNAME, DB_PASSWORD, JWT_SECRET, JWT_EXPIRATION_MINUTES, JWT_REFRESH_EXPIRATION_DAYS, RATE_LIMIT_REQUESTS_PER_MINUTE, RATE_LIMIT_AUTH_REQUESTS_PER_MINUTE, RATE_LIMIT_UNAUTHENTICATED_REQUESTS_PER_MINUTE, OPENWEATHERMAP_API_KEY, SWAGGER_ENABLED) β€” the values in application.yml are local-development defaults only and must be overridden in any real deployment. JWT_SECRET has no default and must always be set; every other variable falls back to a sensible local default if left unset.

Without OPENWEATHERMAP_API_KEY set, the second provider fails with 401 on every real call β€” that's expected, not a bug: the app keeps working normally because fallback always lands on Open-Meteo. To exercise the second provider for real, grab a free OpenWeatherMap key and export it as OPENWEATHERMAP_API_KEY.

The API is available at http://localhost:8080, with Swagger documentation at http://localhost:8080/swagger-ui/index.html.

βœ… Tests

mvn test

Repository tests and the end-to-end security/fallback tests run against a real PostgreSQL database (weather_api_test), not H2, so constraints (unique email, unique favorite per user) are verified the same way they'll behave in production. The circuit breaker test forces a real transition into the OPEN state (via WireMock) and confirms the provider stops being called while it's open.

πŸ“ Notes

  • Swagger UI / OpenAPI docs (/swagger-ui.html, /v3/api-docs) are on by default (SWAGGER_ENABLED unset or true) but disabled on the live Railway deployment (SWAGGER_ENABLED=false) β€” the routes it documents don't leak anything on their own, but publishing the full endpoint map to anyone unauthenticated isn't worth it on a real deployment; run the app locally to browse it interactively.
  • Open-Meteo's geocoding picks the most relevant result by name; ambiguous city names can return the wrong location (no country/coordinate disambiguation yet).
  • Rate limiting, circuit breaker state and cached data are all in-memory and per instance (Caffeine); none of it is shared across multiple application instances yet.
  • Forecast is Open-Meteo-only β€” OpenWeatherMap has no forecast call wired up in this codebase, so there's no fallback for /weather/forecast (a provider outage there surfaces as 502, unlike /weather, which falls back to the secondary provider).

πŸ“„ License

MIT β€” see LICENSE.


Developed by David ArsΓ©nio Martins 🌐 ividi.dev Β· πŸ’» github.com/VidiPT89

About

Weather aggregator API with automatic multi-provider fallback, circuit breaker, JWT auth and per-user history/favorites, built with Spring Boot, PostgreSQL and Resilience4j.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages