Skip to content

Repository files navigation

🎬 Movie API

CI

REST API for managing movies and directors. Node.js + TypeScript (Fastify), MongoDB via Mongoose, Redis for caching, Grafana/Loki for log aggregation, delivered as a Docker Compose stack.

πŸš€ Quick Start

docker compose up -d --build
Service URL
API http://localhost:3000/api
Swagger UI http://localhost:3000/docs
Health http://localhost:3000/health
Grafana http://localhost:3001 (admin/admin)
# Load sample data (3 directors, 5 movies)
docker compose exec app node dist/scripts/seed.js

curl http://localhost:3000/api/movies

πŸ›οΈ Architecture

The code is organised in layers, each depending only on the one beneath it. A request travels downwards and data travels back up, converted at exactly one boundary per concern.

HTTP        src/http/         routes β†’ controllers β†’ response envelope
  β”‚         Route JSON Schemas validate input and serialize output.
  β–Ό         Controllers hold no logic and no try/catch.
Service     src/services/     business rules, caching, error semantics
  β”‚         Decides what a "movie" means: which director is valid,
  β–Ό         what makes a conflict, when the cache is stale.
Repository  src/repositories/ data access + document β†’ DTO mapping
  β”‚         The only layer that speaks Mongoose. Scopes every read
  β–Ό         to non-deleted records.
Database    src/db/           typed Mongoose models and connection

Supporting modules:

Path Responsibility
src/domain/ Zod schemas: one definition drives TypeScript types, runtime validation and Swagger docs
src/cache/ Redis client and the namespaced cache facade
src/config/ Validated environment, logger, CORS and Swagger setup
src/shared/ Error hierarchy, pagination helpers, schema conversion
src/scripts/ seed, cache:flush
ops/ Loki, Promtail and Grafana configuration

Design decisions

One schema per concept. Each domain entity is defined once as a Zod schema. The TypeScript type, the request validation and the OpenAPI documentation are all derived from it, so they cannot drift apart.

Movies reference directors by ID. Movie.director holds a Director UUID, and the service rejects a write pointing at a director that does not exist (400). Deleting a director that movies still reference is refused with 409 unless force=true is passed.

Errors are mapped in one place. Controllers let errors propagate. src/http/error-handler.ts turns an AppError into its status code, a schema violation into 400 with field details, and anything unexpected into a 500 whose message is masked, so internals never reach the client.

Writes invalidate, they do not patch. An earlier version kept the cached collection in sync by pushing and filtering entries in place. Two concurrent writers read-modify-write the same key and the loser's change disappears, so any write now drops the whole namespace instead. The cost is one extra database read; the benefit is that the cache cannot go stale.

The cache is optional. Every Redis operation degrades to a miss on failure. A Redis outage makes the API slower, not unavailable. Startup continues without it and /health reports it as down.

buildApp() has no side effects. Constructing the application opens no connection and binds no port, which lets the test suite drive the real server through app.inject(). src/server.ts owns connections, listening and graceful shutdown.

Deletes are soft by default. DELETE sets isDeleted; ?force=true removes the document. Repositories scope every read to isDeleted: false, so a soft deleted record is invisible above that layer.

πŸ“± API

Full request/response documentation, including schemas and example payloads, is served at /docs.

GET    /api/movies?page=1&limit=20   # List movies (paginated)
GET    /api/movies/:id               # Movie details
POST   /api/movies                   # Create a movie
PUT    /api/movies/:id               # Replace a movie
DELETE /api/movies/:id?force=false   # Delete a movie

GET    /api/directors?page=1&limit=20
GET    /api/directors/:id
POST   /api/directors
PUT    /api/directors/:id
DELETE /api/directors/:id?force=false

GET    /health                       # Dependency status

Response envelope

Every response shares one shape, so clients branch on status rather than on HTTP status alone.

// 200 / 201
{ "status": "success", "message": "Movie retrieved successfully", "data": { /* ... */ } }

// 4xx / 5xx
{ "status": "error", "message": "Movie 3f2a... not found" }

// 400 from schema validation also carries field-level detail
{ "status": "error", "message": "Request validation failed", "details": ["/rating: must be <= 10"] }

Collection endpoints wrap their results:

{ "items": [ /* ... */ ], "total": 42, "page": 1, "limit": 20, "totalPages": 3 }

Status codes

Code Meaning
200 Success
201 Created
400 Schema validation failed, or a movie references a non-existent director
404 Resource does not exist, or was already deleted
409 Duplicate IMDb ID, or a director still referenced by movies
500 Unexpected failure. Message masked, correlation id in x-request-id

Example

# Create a director, then a movie that references it
DIRECTOR_ID=$(curl -s -X POST http://localhost:3000/api/directors \
  -H 'Content-Type: application/json' \
  -d '{"firstName":"Denis","lastName":"Villeneuve","birthDate":"1967-10-03","bio":"Canadian filmmaker."}' \
  | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')

curl -X POST http://localhost:3000/api/movies \
  -H 'Content-Type: application/json' \
  -d "{\"title\":\"Dune\",\"description\":\"A noble family becomes embroiled in a war over the galaxy's most valuable asset.\",\"releaseDate\":\"2021-10-22\",\"genre\":\"Adventure, Drama, Sci-Fi\",\"rating\":8.0,\"imdbId\":\"tt1160419\",\"director\":\"$DIRECTOR_ID\"}"

πŸ› οΈ Development

npm install
cp .env.example .env     # every value has a working default

npm run dev              # watch mode
npm run seed             # sample data
npm run cache:flush      # drop all cached entries

npm test                 # unit + endpoint tests
npm run test:coverage
npm run lint
npm run typecheck

MongoDB and Redis are expected on localhost in development; docker compose up -d mongo redis starts just those two.

Testing

Suite Scope
src/__tests__/unit/ Service rules in isolation, repositories mocked
src/__tests__/endpoints/ Full request lifecycle via app.inject(), only repositories mocked

Endpoint tests exercise real routing, schema validation, serialization, caching and error mapping, so a broken status code or envelope fails the build. Redis is replaced by an in-memory manual mock (src/cache/__mocks__/redis.client.ts) that behaves like a real cache, which is what lets the suite assert on hits, misses and invalidation without a running server.

βš™οΈ Configuration

All environment variables are declared and validated in src/config/env.ts. Startup fails with a readable message if any value is invalid, rather than surfacing later as a NaN port.

Variable Default Notes
NODE_ENV development production switches logs to JSON stdout
HOST / PORT 0.0.0.0 / 3000
API_PREFIX /api
MONGO_HOST localhost Compose overrides to mongo
MONGO_PORT 27017
MONGO_DATABASE movie-db
REDIS_URL redis://localhost:6379 Compose overrides to redis://redis:6379
CACHE_TTL_SECONDS 900 15 minutes
LOG_LEVEL info
LOG_PATH ./logs/app.log Development only

πŸ“Š Logging and Monitoring

Every request is logged twice, on arrival and on completion, sharing one correlation id that is also returned to the caller in the x-request-id header.

In production the application writes JSON to stdout. Promtail discovers containers through the Docker API and ships the logs of any container carrying the logging.job Compose label, so no shared log volume is needed and the API process runs unprivileged.

# Grafana β†’ Explore β†’ Loki
{job="movie-api"}                  # All application logs
{job="movie-api", level="error"}   # Errors only
{job="movie-api"} |= "request completed"

"no org id" error in Grafana

Loki runs multi-tenant, so requests need an X-Scope-OrgID header. It is provisioned in ops/grafana-datasource.yml, but if the datasource was created before that file was mounted:

  1. Connections β†’ Data sources β†’ Loki
  2. Under HTTP β†’ Custom HTTP Headers, add X-Scope-OrgID: 1
  3. Save & Test

🐳 Docker

The image is built in two stages: TypeScript is compiled with the full dependency tree, then only dist/ and production dependencies are copied into the runtime layer, which runs as the unprivileged node user and declares a HEALTHCHECK against /health.

docker compose up -d --build     # Start everything
docker compose logs -f app       # Follow application logs
docker compose ps                # Health status per service
docker compose down              # Stop
docker compose down -v           # Stop and drop volumes

The API waits for MongoDB and Redis to report healthy before starting, so a cold up does not crash-loop while Mongo initialises.

πŸ“„ License

MIT

About

A RESTful API for managing movies and directors with Redis caching, logging, and monitoring capabilities. Built with Node.js, Fastify, and MongoDB. Features include CRUD operations, Redis caching, Loki logging, Grafana monitoring, Docker containerization.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages