Professional GraphQL API for managing property records with integrated weather data from Weatherstack API.
src/
βββ entities/ # TypeORM entities (Database models)
β βββ property.entity.ts
βββ repositories/ # Repository Pattern (Data access layer)
β βββ property.repository.ts
βββ services/ # Business logic layer
β βββ property.service.ts
β βββ weather.service.ts (Singleton)
βββ resolvers/ # GraphQL resolvers layer
β βββ property.resolvers.ts
βββ decorators/ # Method decorators
β βββ error-handler.ts (@HandleErrors)
βββ errors/ # Custom error classes
β βββ custom-errors.ts
βββ graphql/ # GraphQL schema definitions
β βββ schema.ts
βββ types/ # TypeScript interfaces & DTOs
β βββ property.types.ts
β βββ weather.types.ts
βββ utils/ # Utilities (Logger, etc.)
β βββ logger.ts
βββ data-source.ts # TypeORM configuration
βββ index.ts # Application entry point
- Repository Pattern: Encapsulates database operations
- Singleton Pattern: WeatherService instance management
- Decorator Pattern: @HandleErrors for centralized error handling and logging
- Dependency Injection: Services injected for testability
- Custom Error Classes: Type-safe error handling (ValidationError, NotFoundError, WeatherAPIError, DatabaseError)
- Single Responsibility: Each class has one clear purpose (max 20-30 lines per function)
- Open/Closed: Extensible via decorators and interfaces
- Dependency Inversion: Services depend on interfaces (IPropertyService, IWeatherService)
- Interface Segregation: Clean, focused interfaces with I-prefix naming convention
- Node.js (v16+)
- PostgreSQL database (local or AWS RDS)
- Weatherstack API key (Get one here)
npm install- Copy
.env.exampleto.env:
cp .env.example .env- Update environment variables in
.env:
# PostgreSQL
DB_HOST=your-database.region.rds.amazonaws.com
DB_PORT=5432
DB_NAME=weather_app_db
DB_USER=your_username
DB_PASSWORD=your_password
DB_SSL=true
DB_SCHEMA=public
# Weatherstack API
WEATHERSTACK_API_KEY=your_api_key_here
# Application
PORT=4000
APP_HOST=localhost
APP_PROTOCOL=http
NODE_ENV=development
# Simple Rate Limiting (in-memory)
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW_MS=60000TypeORM will automatically create the properties table on first run (when synchronize: true in development).
Production: Use migrations instead of synchronize.
npm run devServer will start at: http://localhost:4000/graphql
npm run build
npm start| Field | Type | Description |
|---|---|---|
| id | UUID | Auto-generated unique identifier |
| street | String | Full street address |
| city | String | City name |
| state | String (2) | Two-letter state code (e.g., AZ) |
| zipCode | String (5) | Five-digit ZIP code |
| weatherData | JSON | Weather info from Weatherstack (on creation) |
| lat | Decimal | Latitude from Weatherstack |
| long | Decimal | Longitude from Weatherstack |
| createdAt | Timestamp | Auto-generated creation timestamp |
Step 1: Start the server
npm run devServer will be available at: http://localhost:4000/graphql
Step 2: Open GraphQL Playground
Open your browser and navigate to http://localhost:4000/graphql. You'll see the GraphiQL interface.
Step 3: Create your first property
Paste this mutation into the left panel:
mutation CreateProperty {
createProperty(input: {
street: "350 5th Ave"
city: "New York"
state: "NY"
zipCode: "10118"
}) {
id
street
city
state
zipCode
weatherData
lat
long
createdAt
}
}Click the "Execute" button (βΆ). You should get a response like:
{
"data": {
"createProperty": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"street": "350 5th Ave",
"city": "New York",
"state": "NY",
"zipCode": "10118",
"weatherData": {
"temperature": 45,
"weather_descriptions": ["Partly cloudy"],
"humidity": 65,
"wind_speed": 10,
"observation_time": "02:30 PM",
"feelslike": 42
},
"lat": 40.748,
"long": -73.986,
"createdAt": "2025-11-29T23:30:00.000Z"
}
}
}Step 4: Query all properties
query GetAllProperties {
properties {
id
street
city
state
weatherData
}
}Expected response:
{
"data": {
"properties": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"street": "350 5th Ave",
"city": "New York",
"state": "NY",
"weatherData": {
"temperature": 45,
"weather_descriptions": ["Partly cloudy"],
"humidity": 65,
"wind_speed": 10,
"observation_time": "02:30 PM",
"feelslike": 42
}
}
]
}
}Step 5: Query single property by ID
Copy the id from the previous response and use it:
query GetProperty {
property(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890") {
id
street
city
weatherData
lat
long
}
}Step 6: Delete a property
mutation DeleteProperty {
deleteProperty(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890")
}Expected response:
{
"data": {
"deleteProperty": true
}
}properties(filter, sort)- Get all properties with optional filtering/sortingproperty(id)- Get single property by ID
createProperty(input)- Create new property (fetches weather data)deleteProperty(id)- Delete property by ID
See GRAPHQL_EXAMPLES.md for additional query examples including:
- Filtering by city, state, zipCode
- Sorting by creation date (ASC/DESC)
- Combined filters and sorting
- Error handling examples
Important: Weatherstack API is called only during property creation (in the createProperty mutation). The weather data, latitude, and longitude are stored in the database, so subsequent queries do not require additional API calls.
{
temperature: number;
weather_descriptions: string[];
humidity: number;
wind_speed: number;
observation_time: string;
feelslike: number;
}Input validation using class-validator:
- street: Required, non-empty string
- city: Required, non-empty string
- state: Exactly 2 uppercase letters (e.g., AZ)
- zipCode: Exactly 5 digits
β
Environment variables for sensitive data
β
SSL/TLS for PostgreSQL connection
β
Input validation with class-validator
β
Structured error handling with @HandleErrors decorator
β
Custom error classes for type safety
β
Structured logging with Winston (silent in tests)
β
Graceful shutdown handlers
β
TypeScript for type safety
β
Connection pooling via TypeORM
β
Interface-based architecture (I-prefix convention)
β
ESLint with TypeScript best practices
β
Kebab-case file naming convention
Uses winston for structured logging with context-aware helpers:
- Contexts: api, database, repository, service, graphql, error
- Levels: info, warn, error (with stack traces)
- Transports: Console + file (logs/combined.log, logs/error.log)
- Test Environment: Automatically silenced when NODE_ENV=test
- DRY (Don't Repeat Yourself): Eliminated duplicate code (~60 lines removed)
- Single Responsibility: Each function has one clear purpose
- Short Functions: Max 20-30 lines per function
- No Logic in Resolvers: Pure delegation to services
- Centralized Error Handling: @HandleErrors decorator replaces try-catch blocks
- Structured Logging: Context-based logging with minimal noise
Professional TypeScript rules enforced:
one-var: consecutive- Grouped variable declarationsindent: 2- Consistent 2-space indentationquotes: single- Single quotes for stringsprefer-const- Immutability by defaultno-var- Modern ES6+ syntax@typescript-eslint/naming-convention- I-prefix for interfaces@typescript-eslint/no-explicit-any: warn- Type safety encouraged
Comprehensive automated test suite covering business logic, API integration, and error handling.
src/__tests__/
βββ services/
β βββ property.service.test.ts # Property business logic tests (17 tests)
β βββ weather.service.test.ts # Weatherstack API integration tests (8 tests)
βββ resolvers/
β βββ property.resolvers.test.ts # GraphQL resolver tests (20 tests)
βββ integration/
βββ graphql-db.integration.test.ts # End-to-end DB persistence tests (2 tests)
# Run all tests
npm test
# Run tests in watch mode (auto-rerun on changes)
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Run specific test file
npm test -- property.service.test.ts
npm test -- weather.service.test.ts
npm test -- property.resolvers.test.ts
# Run integration tests (requires PostgreSQL)
$env:RUN_INTEGRATION_TESTS='true'; npm run test:integrationTotal: 47 tests passing
- β Singleton pattern - returns the same instance on multiple calls
- β Successfully fetch weather data and coordinates
- β Throw error on invalid API response (missing location)
- β Throw error on invalid API response (missing current)
- β Reject non-USA locations
- β Retry on timeout and eventually succeed
- β Not retry on 4xx client errors
- β Fail after max retries on persistent errors
- β createProperty - successfully create a property with weather data
- β createProperty - fail validation with invalid state (not 2 letters)
- β createProperty - fail validation with invalid state (lowercase)
- β createProperty - fail validation with invalid zipCode (not 5 digits)
- β createProperty - fail validation with invalid zipCode (contains letters)
- β createProperty - fail validation with empty street
- β createProperty - abort operation when weather API fails (requirement #4)
- β getAllProperties - return all properties without filters
- β getAllProperties - return filtered properties by city
- β getAllProperties - return sorted properties
- β getPropertyById - return property by ID
- β getPropertyById - throw error when property not found
- β getPropertyById - throw error when id is empty string
- β deleteProperty - successfully delete property
- β deleteProperty - throw error when property to delete not found
- β deleteProperty - throw error when id is empty string
- β deleteProperty - throw error when id is whitespace only
- β query: properties - returns all properties without filters
- β query: properties - filters by city
- β query: properties - filters by state
- β query: properties - filters by zipCode
- β query: properties - sorts by creation date descending
- β query: properties - sorts by creation date ascending
- β query: properties - combines filters and sorting
- β query: properties - returns empty array when no matches
- β query: property by ID - returns property with all details
- β query: property by ID - throws error when ID does not exist
- β query: property by ID - includes weather data
- β query: property by ID - includes coordinates
- β mutation: createProperty - creates property with weather data automatically
- β mutation: createProperty - rejects invalid state format
- β mutation: createProperty - rejects invalid zipCode format
- β mutation: createProperty - rejects missing required fields
- β mutation: createProperty - aborts when weather API fails
- β mutation: deleteProperty - deletes existing property
- β mutation: deleteProperty - throws error when property does not exist
- β mutation: deleteProperty - throws error when id is empty string
- β End-to-end GraphQL mutation β PostgreSQL persistence verification
- β Prevents deletion of non-existent property (UUID validation, no DB side effects)
- Clean Output: All Winston logs silenced in test environment (NODE_ENV=test)
- Mocked Dependencies: axios, repositories isolated for unit testing
- GraphQL API Coverage: All queries and mutations tested with realistic scenarios
- Retry Logic Validation: Confirms 3-attempt retry with exponential backoff (1s, 2s delays)
- Error Path Coverage: Tests 4xx no-retry, 5xx retry behavior, ID validation
- USA-Only Validation: Rejects properties outside United States
- Requirement Validation: Explicit test for "abort operation on weather failure" (Requirement #4)
- Security Tests: Empty ID, whitespace ID, non-existent UUID validation
- Naming Convention: Lowercase describe blocks for consistency
PostgreSQL-backed GraphQL integration test verifies mutation β DB persistence. By default, integration tests are skipped.
Enable integration tests:
$env:RUN_INTEGRATION_TESTS='true'; npm testRun only integration tests:
$env:RUN_INTEGRATION_TESTS='true'; npx jest src/__tests__/integrationNotes:
- Jest loads env and decorators globally via
setupFiles(dotenv/config, reflect-metadata). - Requires Postgres env vars (
.env) and access to the DB.
Lightweight per-IP rate limiting is enabled for /graphql using an in-memory counter (no extra deps), configurable via env:
RATE_LIMIT_MAX: requests per window per IP (default 60)RATE_LIMIT_WINDOW_MS: window size in ms (default 60000)
Disabled in tests (NODE_ENV=test). For production-scale environments, consider a Redis-backed limiter.
express- Web frameworkgraphql&express-graphql- GraphQL servertypeorm- ORM for PostgreSQLpg- PostgreSQL driverclass-validator- Input validationaxios- HTTP client for Weatherstack APIwinston- Structured loggingdotenv- Environment variablesreflect-metadata- TypeScript decorators support
typescript- Type safetyts-node- Run TypeScript directlynodemon- Auto-restart on changesjest&ts-jest- Testing framework@types/jest- Jest TypeScript definitions@types/node- Node.js TypeScript definitionseslint&@typescript-eslint- Code quality linting
{
"dev": "nodemon src/index.ts",
"dev:debug": "nodemon --inspect src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts"
}- GraphQL:
http://localhost:4000/graphql - Health Check:
http://localhost:4000/health
MIT