A monolithic TypeScript application that retrieves weather forecast data from the OpenWeatherMap API, persists it in a PostgreSQL database with Redis caching, and serves it through both a server-rendered web dashboard and a RESTful API β no authentication required.
- Summary
- Features
- Tech Stack
- Architecture
- Folder Structure
- Prerequisites
- Setup Guide
- API Endpoints
- Dashboard
- Caching Strategy
- Scheduled Jobs
- Testing
- Environment Variables
- License
The Weather Forecast Service allows visitors to look up current weather conditions and forecasts for any city worldwide. Data is sourced from the OpenWeatherMap API, stored in PostgreSQL via Drizzle ORM, and cached in Redis to minimise external API calls and improve response times.
Key capabilities include:
- Current Weather β temperature, humidity, wind speed/direction, pressure, sunrise/sunset, and conditions for a given city.
- Weather Forecast β short-term forecast data including temperature, humidity, wind, rain volume, and precipitation probability.
- Web Dashboard β an EJS-rendered frontend with a search form, recent searches list, and detailed weather view.
- REST API β JSON endpoints for programmatic access to current weather and forecast data.
- Background Refresh β scheduled cron jobs that automatically update weather data for frequently searched cities.
- Graceful Degradation β a layered data-fetching strategy (Cache β Database β API) ensures the service remains responsive even when the external API is unavailable.
| Feature | Description |
|---|---|
| City search | Look up weather by exact city name |
| Multi-layer data fetching | Redis cache β PostgreSQL β OpenWeatherMap API fallback chain |
| Automatic data refresh | Cron jobs update current weather (every 2 hrs) and forecasts (every 3 hrs) |
| Server-side rendering | EJS templates for a visitor-friendly dashboard |
| Structured logging | Winston + Logtail (Better Stack) for local and remote log aggregation |
| Standardised responses | Consistent JSON response format via a shared response handler |
| Global error handling | Centralised Express error middleware with network error detection |
| Environment-aware config | Separate database and Redis URLs for development, test, and production |
| Technology | Purpose |
|---|---|
| TypeScript | Primary language |
| Express 5 | Web framework |
| Drizzle ORM | Type-safe PostgreSQL ORM & migrations |
| PostgreSQL | Relational database |
| Redis | In-memory cache |
| node-fetch | HTTP client for OpenWeatherMap API |
| node-schedule | Cron-style scheduled tasks |
| Winston | Logging framework |
| Logtail | Remote log transport (Better Stack) |
| Joi | Request validation schemas |
| CORS | Cross-origin resource sharing |
| dotenv | Environment variable management |
| Technology | Purpose |
|---|---|
| EJS | Server-side HTML templating |
| Tailwind CSS 4 | Utility-first CSS framework |
| Technology | Purpose |
|---|---|
| tsx | TypeScript execution & watch mode |
| Jest | Unit & integration testing |
| SuperTest | HTTP assertion library for API tests |
| Drizzle Kit | Database migration tooling |
The application follows a layered architecture within a monolithic Express application:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Weather Forecast Service β
β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Web Layer β βService Layer β β Data Layer β β
β β(Controllers/ββββΊβ (Services) ββββΊβ (Repos / β β
β β Views) β β β β Cache) β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β² β² β² β
ββββββββββββΌββββββββββββββββββΌβββββββββββββββββΌββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββββ ββββββββββββββββ
β Web Browser β β OpenWeatherMap β β PostgreSQL β
ββββββββββββββββ β API β β + Redis β
ββββββββββββββββββ ββββββββββββββββ
Data-fetching waterfall: Every read request follows a three-tier strategy:
- Redis Cache β fastest; returns immediately if a cache hit is found.
- PostgreSQL β checked next; results are cached on retrieval.
- OpenWeatherMap API β last resort; data is persisted to both DB and cache.
Weather Forecast Service/
βββ drizzle/ # Drizzle-Kit generated migrations
β βββ 0000_square_sir_ram.sql # Initial migration SQL
β βββ meta/ # Migration metadata
βββ public/ # Static assets served by Express
β βββ input.css # Tailwind CSS source
β βββ output.css # Compiled Tailwind CSS
β βββ bitimg.jpg # Image asset
βββ views/ # EJS templates
β βββ home.ejs # Dashboard β search form & recent cities
β βββ weather.ejs # Weather detail β current + forecast view
βββ src/
β βββ index.ts # Application entry point (starts server)
β βββ app.ts # Express app setup (middleware, routes, crons)
β βββ configs/
β β βββ db.config.ts # PostgreSQL pool & Drizzle initialisation
β β βββ cache.config.ts # Redis client with reconnection strategy
β β βββ logger.config.ts # Winston + Logtail logger configuration
β βββ db/
β β βββ schema.ts # Re-exports all Drizzle schemas
β βββ middleware/
β β βββ errorHandler.ts # Global Express error handler
β βββ modules/
β β βββ weather/
β β βββ weather.routes.ts # Route definitions (API + Dashboard)
β β βββ weather.controller.ts # Request handlers
β β βββ weather.service.ts # Business logic & DB operations
β β βββ weather.api.ts # OpenWeatherMap API integration
β β βββ weather.cache.ts # Redis cache operations
β β βββ weather.cron.ts # Scheduled background jobs
β β βββ weather.schema.ts # Drizzle table definitions
β β βββ weather.middleware.ts # Route-level middleware (placeholder)
β β βββ tests/
β β βββ weather.integration.test.ts
β β βββ fixtures/ # Test fixtures
β β βββ tsconfig.json # Test-specific TS config
β βββ types/
β β βββ weather.d.ts # TypeScript type declarations
β βββ utils/
β βββ responseHandler.ts # Standardised JSON response helper
β βββ isStringArray.ts # Type guard utility
βββ .env # Environment variables
βββ .gitignore
βββ drizzle.config.ts # Drizzle Kit configuration
βββ jest.config.js # Jest test configuration
βββ tsconfig.json # TypeScript compiler options
βββ package.json
βββ PRD.md # Product Requirements Document
βββ TDD.md # Technical Design Document
βββ todo.md # Development notes & learnings
Before setting up the project, ensure you have the following installed:
- Node.js β₯ 18
- PostgreSQL β running locally or a remote instance
- Redis β running locally or a remote instance
- OpenWeatherMap API key β sign up at openweathermap.org
git clone https://github.com/MaxKolbe/Weather-Forecast-Service.git
cd Weather-Forecast-Servicenpm installCreate a .env file in the project root (see Environment Variables for the full list):
NODE_ENV=development
# PostgreSQL
PG_DATABASE_DEV_URL=postgresql://postgres:password@localhost:5432/devdb
PG_DATABASE_TEST_URL=postgresql://postgres:password@localhost:5432/testdb
PG_DATABASE_PROD_URL=<your-production-database-url>
# Redis
REDIS_DEV_URL=redis://localhost:6379
REDIS_TEST_URL=redis://localhost:6379
REDIS_PROD_URL=<your-production-redis-url>
# OpenWeatherMap
WEATHER_APIKEY=<your-api-key>
# Logging
LOG_LEVEL=debug
SOURCE_TOKEN=<your-logtail-source-token>
INGESTING_HOST=<your-logtail-ingesting-host>
PORT=3000Generate and apply the database schema:
npm run db:gen-migOr run each step separately:
npm run db:generate # Generate migration files
npm run db:migrate # Apply migrationsnpm run stylenpm run devThe server will start at http://localhost:3000.
npm run build # Compile TypeScript to dist/
npm run start # Run the compiled applicationAll API routes are prefixed with /api/v1/weather.
GET /api/v1/weather/current?city={cityName}
Returns the current weather conditions for the specified city.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
city |
string | Yes | City name (case-insensitive) |
Success Response 200 OK:
{
"status": 200,
"message": "Success: Current Weather found",
"data": {
"city": "berlin",
"country": "DE",
"timestamp": "2025-03-21T14:30:00.000Z",
"temperature": 12.5,
"humidity": 65,
"windSpeed": 5.2,
"windDirection": 180,
"pressure": 1012,
"conditions": "Clouds",
"description": "scattered clouds",
"sunrise": "2025-03-21T06:12:00.000Z",
"sunset": "2025-03-21T18:34:00.000Z"
}
}Error Responses:
| Status | Condition |
|---|---|
400 |
Missing or empty city parameter |
404 |
City not found |
503 |
Network error (DNS resolution failure) |
504 |
Request to weather API timed out |
GET /api/v1/weather/forecast?city={cityName}
Returns forecast data for the specified city.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
city |
string | Yes | City name (case-insensitive) |
Success Response 200 OK:
{
"status": 200,
"message": "Success: Weather Forecast found",
"data": {
"city": "berlin",
"country": "DE",
"forecast": {
"date": "2025-03-22T12:00:00.000Z",
"temperature": 14.2,
"humidity": 60,
"windSpeed": 4.8,
"conditions": "Clear",
"description": "clear sky"
}
}
}Error Responses:
| Status | Condition |
|---|---|
400 |
Missing or empty city parameter |
404 |
City not found |
503 |
Network error (DNS resolution failure) |
504 |
Request to weather API timed out |
| Method | Route | Description |
|---|---|---|
GET |
/api/v1/weather/home |
Renders the home dashboard with recent searches |
GET |
/api/v1/weather/?city={cityName} |
Renders the weather detail page for a city |
The web dashboard provides a visitor-friendly interface built with EJS and Tailwind CSS:
- Home Page (
/api/v1/weather/home) β a search form to enter a city name, along with a list of recently searched cities pulled from the Redis cache. - Weather Detail Page (
/api/v1/weather/?city=berlin) β displays current weather conditions and forecast data side by side, fetched in parallel viaPromise.all.
| Data Type | Cache Key Pattern | TTL |
|---|---|---|
| Current Weather | get:currentweather:{city} |
15 minutes |
| Forecast | get:forecast:{city} |
1 hour |
| City Name | get:city:{city} |
24 hours |
The Redis client is configured with an exponential backoff reconnection strategy (with jitter) and a maximum of 5 retries to handle transient connection failures gracefully.
Background cron jobs keep weather data fresh for frequently searched cities:
| Job | Schedule | Description |
|---|---|---|
updateCurrentWeatherCron |
Every 2 hours | Batch-updates current weather for all recently searched cities |
updateForecastCron |
Every 3 hours | Batch-updates forecast data for all recently searched cities |
Both jobs identify "frequently searched" cities by reading cached city keys and filtering for those searched within the last 12 hours.
The project uses Jest with SuperTest for integration testing.
# Run the test suite
npm testTests are located at src/modules/weather/tests/ and cover:
- Integration tests β end-to-end API endpoint testing with
SuperTest - Fixtures β reusable test data in the
fixtures/directory
Note: Tests run with
--experimental-vm-modulesfor ES module support and--detectOpenHandlesto catch unclosed async operations.
| Variable | Description |
|---|---|
NODE_ENV |
Environment: development, test, or production |
PORT |
Server port (default: 3000) |
PG_DATABASE_DEV_URL |
PostgreSQL connection string (development) |
PG_DATABASE_TEST_URL |
PostgreSQL connection string (test) |
PG_DATABASE_PROD_URL |
PostgreSQL connection string (production) |
REDIS_DEV_URL |
Redis connection string (development) |
REDIS_TEST_URL |
Redis connection string (test) |
REDIS_PROD_URL |
Redis connection string (production) |
WEATHER_APIKEY |
OpenWeatherMap API key |
LOG_LEVEL |
Winston log level (default: info) |
SOURCE_TOKEN |
Logtail / Better Stack source token |
INGESTING_HOST |
Logtail ingesting endpoint host |
| Script | Command | Description |
|---|---|---|
npm run dev |
tsx watch src/index.ts |
Start dev server with hot-reload |
npm run build |
npm install --include=dev && npx tsc |
Compile TypeScript to dist/ |
npm run start |
node dist/index.js |
Run the production build |
npm run watch |
npx tsc -w |
Watch-mode TypeScript compilation |
npm run db:push |
npx drizzle-kit push |
Push schema changes directly |
npm run db:generate |
npx drizzle-kit generate |
Generate migration files |
npm run db:migrate |
npx drizzle-kit migrate |
Apply pending migrations |
npm run db:gen-mig |
Generate + migrate in one step | Combined migration command |
npm run style |
Tailwind CLI | Compile Tailwind CSS (watch mode) |
npm test |
Jest | Run the test suite |
MIT