REST API powering Klaps — the Polish nationwide guide to special screenings, classic cinema, and retrospectives.
Getting Started · Project Structure · Deployment
Klaps Backend is the NestJS REST API that serves the Klaps frontend. It manages movies, cinemas, cities, genres, screenings, and showtimes — all ingested by an external scrapper and exposed through a versioned API behind rate limiting and API key authentication.
| Layer | Technology |
|---|---|
| Framework | NestJS 11 (Express) |
| Language | TypeScript 5 |
| ORM | Drizzle ORM (PostgreSQL dialect) |
| Database | PostgreSQL via pg |
| Validation | class-validator + class-transformer |
| Auth | API key guard (x-internal-api-key header) |
| Rate Limiting | @nestjs/throttler (30/10s, 100/60s) |
| Caching | @nestjs/cache-manager |
| Logging | nestjs-pino (structured JSON) |
| Health Checks | @nestjs/terminus |
| Security | Helmet |
| Testing | Jest + @nestjs/testing + Supertest |
| Package Manager | Bun |
| Runtime | Node.js 22 (LTS) |
| Deployment | Docker (multi-stage Alpine) via GitHub Actions to GHCR |
Request ──► Helmet ──► CORS ──► ThrottlerGuard ──► Controller ──► Service ──► Drizzle ──► PostgreSQL
│
InternalBypassThrottlerGuard
(skips limit for internal key)
- Global prefix:
/api/v2 - Global guard:
InternalBypassThrottlerGuard— rate limits public traffic, skips for requests with validx-internal-api-key - Per-route guard:
InternalApiKeyGuard— restricts write endpoints (POST) and some reads to the internal scrapper - Validation pipe:
whitelist: true,forbidNonWhitelisted: true,transform: true
src/
├── cinemas/ # Cinemas module (controller, service, repository, DTOs)
├── cities/ # Cities module
├── database/ # Drizzle setup, schemas, migrations
│ ├── schemas/ # Table definitions & relations
│ ├── migrations/ # Generated SQL migrations
│ └── constants.ts # DRIZZLE injection token
├── genres/ # Genres module
├── guards/ # InternalApiKeyGuard, InternalBypassThrottlerGuard
├── health/ # Health check (Terminus + Drizzle indicator)
├── lib/ # Utilities (pagination, slugs, dates, batch helpers, deadlock retry)
├── logger/ # Pino logger module
├── movies/ # Movies module
├── screenings/ # Screenings module
├── showtimes/ # Showtimes module (ingestion pipeline)
├── socials/ # Socials module (candidate scoring & post tracking)
├── scripts/ # DB scripts (baseline, wipe, slug backfill)
├── app.module.ts # Root module
└── main.ts # Bootstrap (port, CORS, Helmet, pipes)
test/
├── app.e2e-spec.ts # E2E health check test
└── jest-e2e.json # E2E Jest config
All routes are prefixed with /api/v2. All endpoints require the x-internal-api-key header (except /health).
| Method | Route | Description |
|---|---|---|
| GET | /health |
Health check (DB) |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /cities |
— | List all cities |
| GET | /cities/scraped |
?dateFrom, ?dateTo, ?cityId, ?citySlug |
Scraped city IDs in date range |
| GET | /cities/with-cinemas |
— | Cities with cinema count |
| GET | /cities/:slug |
:slug |
City detail + screenings |
| POST | /cities/batch |
CreateCitiesBatchDto |
Batch upsert cities |
| POST | /cities/:slug |
UpdateCityDto |
Update city by slug |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /cinemas |
?cityId, ?citySlug |
List cinemas |
| GET | /cinemas/:slug |
:slug |
Cinema detail with city |
| POST | /cinemas/batch |
CreateCinemasBatchDto |
Batch upsert cinemas |
| POST | /cinemas/:slug |
UpdateCinemaDto |
Update cinema by slug |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /genres |
— | List all genres |
| GET | /genres/:slug |
:slug |
Genre detail |
| POST | /genres/:slug |
UpdateGenreDto |
Update genre by slug |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /movies |
?search, ?genreId, ?genreSlug, ?page, ?limit |
Paginated movie list |
| GET | /movies/multi-city |
?limit (1-50) |
Movies screened in the most cities (cached 15min) |
| GET | /movies/:slug |
:slug |
Full movie detail with relations |
| POST | /movies/batch |
CreateMoviesBatchDto |
Batch upsert movies with relations |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /screenings |
?dateFrom, ?dateTo, ?movieId, ?cityId, ?citySlug, ?genreId, ?genreSlug, ?cinemaSlug, ?search |
Screenings (grouped by movie) |
| GET | /screenings/random-screening |
— | Random retro screening |
| POST | /screenings |
CreateScreeningDto |
Create screening |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /showtimes |
?dateFrom, ?dateTo, ?cityId, ?citySlug |
List showtimes |
| POST | /showtimes/batch |
CreateShowtimesBatchDto |
Batch upsert |
| Method | Route | Params / Body | Description |
|---|---|---|---|
| GET | /socials/candidate |
?dateFrom, ?dateTo, ?minScore, ?numberOfCandidates, ?platform |
Get scored candidate |
| POST | /socials/reserve |
SocialsActionDto (platform, screeningId) |
Reserve candidate |
| POST | /socials/publish |
SocialsActionDto (platform, screeningId) |
Publish candidate |
Create a .env file in the project root:
PORT=5000
DATABASE_URL=postgresql://user:password@localhost:5432/klaps_dev
INTERNAL_API_KEY=your-secret-api-key
FRONTEND_URL=http://localhost:3000| Variable | Required | Description |
|---|---|---|
PORT |
No | Server port (default: 5000) |
DATABASE_URL |
Yes | PostgreSQL connection string |
INTERNAL_API_KEY |
Yes | API key for authenticating internal/scrapper requests |
FRONTEND_URL |
No | Allowed CORS origin for the frontend |
LOG_LEVEL |
No | Pino log level: debug, info, warn, error (default: debug dev / info prod) |
LOG_FILE |
No | Path to additional log file output; if unset, logs go to stdout only |
# Install dependencies
bun install
# Generate Drizzle migrations (if schema changed)
bun run db:generate
# Run migrations
bun run db:migrate
# Start in development (watch mode)
bun run start:dev
# Build for production
bun run build
# Start production
bun run start:prod
# Lint
bun run lint
# Unit tests
bun run test
# E2E tests
bun run test:e2e
# Test coverage
bun run test:covThe API will be available at http://localhost:5000/api/v2.
docker build -t klaps-backend .docker run -p 5000:5000 \
-e DATABASE_URL=postgresql://user:pass@host:5432/klaps \
-e INTERNAL_API_KEY=your-key \
-e FRONTEND_URL=https://klaps.space \
klaps-backendThe included docker-compose.yml is configured for deployment behind Traefik reverse proxy with automatic HTTPS and a built-in healthcheck:
docker compose up -dThe compose file expects a .env file and an external proxy network for Traefik.
The project uses GitHub Actions for CI/CD (.github/workflows/deploy.yml):
| Branch | Environment | Image Tag |
|---|---|---|
main |
Production | latest |
dev |
Development | dev |
Pipeline steps:
- Lint & Test — ESLint + Jest unit tests (gates the build)
- Build & Push — Docker image to GitHub Container Registry
- DB Backup —
pg_dumpvia SSH before migration - DB Migrate — Drizzle baseline + migrations
- Deploy — SCP compose file, pull image, recreate container
Required GitHub Secrets:
| Secret | Description |
|---|---|
IMAGE_NAME |
GHCR image (e.g. ghcr.io/user/klaps-backend) |
DATABASE_URL |
PostgreSQL connection string |
INTERNAL_API_KEY |
API authentication key |
SERVER_IP |
Deployment server IP |
SERVER_USER |
SSH user |
SERVER_SSH_KEY |
SSH private key |
PROJECT_DIR |
Remote project root path |
PORT |
Application port |
DOMAIN |
Domain for Traefik routing |
GHCR_PAT |
GitHub Container Registry token |
Klaps is an open-source project. The source code is publicly available on GitHub:
| Component | Repository |
|---|---|
| Frontend (Next.js) | github.com/klaps-hq/klaps |
| Backend (NestJS) | github.com/klaps-hq/api.klaps.space |
The scrapper responsible for collecting screening data is not publicly available for legal reasons.
See CONTRIBUTING.md for guidelines on how to contribute.
This project is licensed under the MIT License.
