Production-ready REST API for high-concurrency ticket sales — built with NestJS, PostgreSQL, Redis, BullMQ, Stripe and WebSockets. Deployed on AWS EC2.
📖 ticket.lsbstack.com/api/docs — Swagger UI (interactive, try it live)
Payments run in Stripe test mode — no real charges.
Option 1 — Regular user (CUSTOMER role)
- Open
POST /auth/register→ click Try it out → Execute with the preloaded example data - Copy the
accessTokenfrom the response - Click Authorize (top right) → paste the token → Authorize
- All endpoints are now available
Option 2 — Admin user (full access)
- Open
POST /auth/login→ click Try it out → use these credentials:
{
"email": "admin@ticketmaster.com",
"password": "Admin1234!"
}- Copy the
accessTokenfrom the response - Click Authorize → paste the token → Authorize
- Admin endpoints (create/update/delete events, manage categories, change user roles) are now unlocked
A production API solving real engineering problems — not a tutorial project.
- Zero overselling under high concurrency — ACID transactions with PostgreSQL ensure exactly 1 successful checkout when 50 users hit the last ticket simultaneously (verified with k6)
- Async payment processing — Stripe Webhooks trigger order confirmation and enqueue email jobs via BullMQ + Redis without blocking the API response
- Real-time stock updates — Socket.io broadcasts
ticket:stock-updatedto all connected clients after each successful purchase - Redis caching —
GET /eventscached for 60s, reducing DB load on high-frequency endpoints - JWT with refresh token rotation — access tokens (15min) + refresh tokens (30d) with rotation on each refresh
- Rate limiting — configurable throttling per endpoint (login: 5 req/min, checkout: 10 req/min, global: 100 req/min)
- Production deployment — AWS EC2 (eu-north-1) + AWS S3 for image storage + Cloudflare DNS + Stripe webhooks configured in production
- Full CI pipeline — GitHub Actions: lint → unit tests (Jest) → API integration tests (Newman) → k6 smoke test
| Technology | Usage |
|---|---|
| NestJS | Backend framework (modular architecture) |
| Prisma | ORM + migrations |
| PostgreSQL | Primary database |
| Redis | Cache (GET /events) + BullMQ queues |
| BullMQ | Async job queues: emails, order expiration |
| Stripe | Payments (PaymentIntents + Webhooks) |
| Resend | Transactional emails |
| Socket.io | WebSockets for real-time stock updates |
| MinIO / AWS S3 | Image storage (MinIO local → S3 in production) |
| k6 | Load testing |
| Newman | API integration tests (CI) |
| Swagger | API documentation (OpenAPI) |
| nestjs-pino | Structured logging |
flowchart TB
subgraph Client
A[HTTP Client / WebSocket]
end
subgraph API
B[Auth: Login/Register]
C[Events: CRUD + Redis Cache]
D[Categories: per event]
E[Orders: Checkout]
F[Webhooks: Stripe]
end
subgraph Services
G[(PostgreSQL)]
H[(Redis)]
I[BullMQ]
J[Stripe]
K[Resend]
L[MinIO/S3]
end
A --> B
A --> C
A --> D
A --> E
A --> F
B --> G
C --> H
C --> G
C --> L
D --> G
E --> G
E --> J
E --> I
F --> E
I --> K
I --> G
sequenceDiagram
participant C as Client
participant API
participant DB as PostgreSQL
participant Stripe
participant BullMQ
participant Email
C->>API: POST /orders/checkout
API->>DB: $transaction (decrement stock + create order)
alt Insufficient stock
DB-->>API: 0 rows updated
API-->>C: 400 Bad Request
else OK
DB-->>API: 1 row updated
API->>Stripe: create PaymentIntent
Stripe-->>API: clientSecret
API-->>C: 201 { clientSecret, orderId }
end
Note over C,Stripe: Client completes payment on frontend
Stripe->>API: Webhook payment_intent.succeeded
API->>DB: Mark order as PAID
API->>BullMQ: Enqueue purchase email job
BullMQ->>Email: Send email
The checkout engine uses prisma.$transaction to guarantee atomicity and prevent overselling.
Without transactions, two users buying the last ticket simultaneously could both succeed:
- User A reads
availableStock = 1 - User B reads
availableStock = 1 - User A decrements → creates order
- User B decrements → creates order → oversell
Inside the transaction:
UPDATE ticket_categories
SET "availableStock" = "availableStock" - ${quantity}
WHERE id = ${categoryId} AND "availableStock" >= ${quantity}- Only updates if stock is sufficient (
availableStock >= quantity) - If
updated === 0→ order is not created, error is thrown - If
updated === 1→ order and tickets are created in the same transaction
Scenario 4 (scenario4-acid.js) simulates 50 users buying 1 ticket in a category with stock = 1. The threshold verifies:
acid_successful_checkouts == 1→ ✅ No oversellingacid_successful_checkouts > 1→ ❌ Oversell detected
| Scenario | Users | p95 | req/s | Error rate | Result |
|---|---|---|---|---|---|
| 1 — Ramp-up | 0→500 | 11.32ms | 390.14 | 0% | ✅ |
| 2 — Spike | 1000 | 14.4s | 136.36 | 0% | ✅ |
| 3 — Soak | 200 / 5min | 11.15ms | 247.11 | 0% | ✅ |
| 4 — ACID | 50 / stock=1 | 79.07ms | — | 1 checkout | ✅ |
| 5 — Rate limit | 1 / 7 reqs | — | — | 2× 429 | ✅ |
Full documentation in k6/README.md.
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /auth/register |
Register new user | — |
| POST | /auth/login |
Login (rate limit 5/min) | — |
| POST | /auth/refresh |
Refresh tokens | — |
| POST | /auth/logout |
Logout | — |
| PATCH | /auth/users/:id/role |
Change user role | ADMIN |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /events |
Create event (multipart/form-data) | ADMIN |
| GET | /events |
List published events (paginated, cached) | — |
| GET | /events/:id |
Get event by ID | — |
| PATCH | /events/:id |
Update event | ADMIN |
| PATCH | /events/:id/status |
Change status (PUBLISHED, etc.) | ADMIN |
| DELETE | /events/:id |
Delete event | ADMIN |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /events/:eventId/categories |
Create category | ADMIN |
| GET | /events/:eventId/categories |
List event categories | — |
| DELETE | /events/:eventId/categories/:categoryId |
Delete category | ADMIN |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /orders/checkout |
Create order + PaymentIntent | JWT |
| POST | /orders/:id/refund |
Request refund | JWT |
| GET | /orders/my-orders |
My orders | JWT |
| GET | /orders/:id |
Get order by ID | JWT |
| Method | Route | Description | Auth |
|---|---|---|---|
| POST | /webhooks/stripe |
Stripe webhook (raw body) | Stripe signature |
- Node.js 22+
- pnpm 10+
- Docker and Docker Compose
git clone https://github.com/LucasBenitez7/ticketmaster-api.git
cd ticketmaster-api
pnpm installdocker-compose up -d- Open http://localhost:9001
- Login:
minioadmin/minioadmin - Create a bucket named
ticketmaster
cp .env.example .envpnpm db:deploy
pnpm db:seedpnpm start:devAPI available at http://localhost:3000 — Swagger at http://localhost:3000/api/docs
stripe listen --forward-to localhost:3000/webhooks/stripe| Variable | Description | Example |
|---|---|---|
PORT |
Server port | 3000 |
DATABASE_URL |
PostgreSQL URL | postgresql://admin:pass@localhost:5435/ticket_db |
JWT_SECRET |
JWT secret | your-secret |
ACCESS_TOKEN_EXPIRES_IN |
Access token expiry | 15m |
REDIS_HOST |
Redis host | localhost |
REDIS_PORT |
Redis port | 6379 |
S3_ENDPOINT |
MinIO/S3 URL | http://localhost:9000 |
S3_REGION |
S3 region | us-east-1 |
S3_ACCESS_KEY_ID |
Access key | minioadmin |
S3_SECRET_ACCESS_KEY |
Secret key | minioadmin |
S3_BUCKET_NAME |
Bucket name | ticketmaster |
STRIPE_SECRET_KEY |
Stripe secret key | sk_test_... |
STRIPE_WEBHOOK_SECRET |
Webhook secret | whsec_... |
RESEND_API_KEY |
Resend API key | re_... |
RESEND_FROM |
Sender email | TicketMaster <noreply@lsbstack.com> |
EMAIL_ENABLED |
Enable email sending | true |
THROTTLE_GLOBAL_LIMIT |
Global rate limit req/min | 100 |
THROTTLE_CHECKOUT_LIMIT |
Checkout rate limit req/min | 10 |
| Variable | Development | Production |
|---|---|---|
S3_ENDPOINT |
http://localhost:9000 |
"" (empty for AWS) |
S3_REGION |
us-east-1 |
eu-north-1 |
S3_ACCESS_KEY_ID |
minioadmin |
Your AWS key |
S3_SECRET_ACCESS_KEY |
minioadmin |
Your AWS secret |
S3_BUCKET_NAME |
ticketmaster |
Your bucket name |
# Unit tests (Jest)
pnpm test
# Unit tests with coverage
pnpm test:cov
# API integration tests (Newman)
pnpm test:api
# Load tests (k6)
k6 run k6/scenario1-rampup.jsGitHub Actions runs on every push/PR to development and main:
| Workflow | Description |
|---|---|
| CI | Lint, typecheck and unit tests (Jest) |
| Newman | API integration tests (PostgreSQL + Redis) |
| k6 Smoke | Smoke test with 10 VUs for 30s |
- Collection:
postman/ticketmaster.postman_collection.json - Environment:
postman/ticketmaster.postman_environment.json
pnpm test:api # runs Newman and generates HTML report at postman/results.htmlUNLICENSED