A REST API for managing trips, bookings, payments, and travelers. Built with Node.js, Koa.js, PostgreSQL, and Zod for validation.
You have a maximum of 3 hours to complete this challenge.
We value your time. Focus on demonstrating your ability to move fast using AI tools (Claude, Cursor, Copilot, etc.) while maintaining code quality.
We care more about seeing your progress than having fully working code. We want to see:
- How you structure your code
- How the pieces fit together
- Code quality and simplicity—the simpler, the better
- How you use AI tools effectively
Working code is a plus, but not a requirement. Show us how you think and build.
- Docker and Docker Compose
- Node.js v24+ (for local development without Docker)
- Start the environment:
docker compose upThis will:
- Start a PostgreSQL database
- Start the API server on port 3000 with hot reload
- Run migrations and seeds (in another terminal):
docker compose exec api npm run db:reset- Verify the API is running:
curl http://localhost:3000/health- Install dependencies:
npm install- Copy the environment file:
cp .env.example .env- Start PostgreSQL (using Docker):
docker compose up postgres- Run migrations and seeds:
npm run db:reset- Start the development server:
npm run dev| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| Method | Endpoint | Description |
|---|---|---|
| GET | /trips |
List all trips (filter: ?destination=) |
| GET | /trips/:id |
Get trip by ID |
| POST | /trips |
Create trip |
| PUT | /trips/:id |
Update trip |
| DELETE | /trips/:id |
Delete trip |
| Method | Endpoint | Description |
|---|---|---|
| GET | /travelers |
List all travelers (filter: ?email=) |
| GET | /travelers/:id |
Get traveler by ID |
| POST | /travelers |
Create traveler |
| PUT | /travelers/:id |
Update traveler |
| DELETE | /travelers/:id |
Delete traveler |
| Method | Endpoint | Description |
|---|---|---|
| GET | /bookings |
List all bookings (filter: ?status=, ?trip_id=) |
| GET | /bookings/:id |
Get booking by ID |
| POST | /bookings |
Create booking |
| PUT | /bookings/:id |
Update booking |
| DELETE | /bookings/:id |
Delete booking |
| Method | Endpoint | Description |
|---|---|---|
| GET | /payments |
List all payments (filter: ?status=, ?booking_id=) |
| GET | /payments/:id |
Get payment by ID |
| POST | /payments |
Create payment |
| PUT | /payments/:id |
Update payment |
| DELETE | /payments/:id |
Delete payment |
{
"id": 1,
"title": "Barcelona Adventure",
"destination": "Barcelona",
"start_date": "2025-03-15",
"end_date": "2025-03-20",
"created_at": "2025-01-01T00:00:00.000Z"
}{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"created_at": "2025-01-01T00:00:00.000Z"
}{
"id": 1,
"trip_id": 1,
"traveler_id": 1,
"status": "confirmed",
"created_at": "2025-01-01T00:00:00.000Z"
}Status values: pending, confirmed, cancelled
{
"id": 1,
"booking_id": 1,
"amount": 500.00,
"currency": "EUR",
"status": "completed",
"created_at": "2025-01-01T00:00:00.000Z"
}Status values: pending, completed, refunded
Bookings and payments have related statuses that reflect the booking flow:
┌─────────────────────────────────────────────────────────────────┐
│ BOOKING FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌───────────┐ ┌───────────┐ │
│ │ pending │ ───► │ confirmed │ ───► │ cancelled │ │
│ └─────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Payment │ │ Payment │ │ Payment │ │
│ │ pending │ ───► │ completed │ ───► │ refunded │ │
│ └─────────┘ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Booking Status | Payment Status | Description |
|---|---|---|
pending |
pending |
Booking created, awaiting payment |
confirmed |
completed |
Payment received, booking confirmed |
cancelled |
completed |
Booking cancelled, payment not yet refunded |
cancelled |
refunded |
Booking cancelled and payment refunded |
- A booking starts as
pendingwhen created - A payment starts as
pendingwhen created - When a payment becomes
completed, the booking should becomeconfirmed - When a booking is
cancelled:- If a
completedpayment exists, a refund should be created (new payment with negative amount andrefundedstatus) - The original payment remains
completedfor audit purposes
- If a
# Run migrations
npm run migrate
# Run seeds
npm run seed
# Reset database (migrate + seed)
npm run db:resetsrc/
├── index.ts # Koa app entry point
├── db.ts # PostgreSQL connection pool
├── models/ # Database operations
│ ├── tripModel.ts
│ ├── travelerModel.ts
│ ├── bookingModel.ts
│ └── paymentModel.ts
├── middleware/ # Request handlers
│ ├── tripsMiddleware.ts
│ ├── travelersMiddleware.ts
│ ├── bookingsMiddleware.ts
│ └── paymentsMiddleware.ts
├── routes/ # Route definitions
│ ├── index.ts
│ ├── trips.ts
│ ├── travelers.ts
│ ├── bookings.ts
│ └── payments.ts
└── schemas/ # Zod validation schemas
├── trip.ts
├── traveler.ts
├── booking.ts
└── payment.ts
All request bodies are validated using Zod schemas. Validation schemas are defined in src/schemas/.
// src/schemas/trip.ts
import { z } from 'zod'
export const tripCreateSchema = z.object({
title: z.string().min(1).max(255),
destination: z.string().min(1).max(255),
start_date: z.string().date(),
end_date: z.string().date(),
})
export type TripCreate = z.infer<typeof tripCreateSchema>// src/middleware/tripsMiddleware.ts
export async function createTrip(ctx: Context) {
const validation = tripCreateSchema.safeParse(ctx.request.body)
if (!validation.success) {
ctx.status = 400
ctx.body = {
error: 'Validation failed',
details: validation.error.flatten().fieldErrors
}
return
}
const trip = await TripModel.create(validation.data)
ctx.status = 201
ctx.body = trip
}When validation fails, the API returns a 400 response with field-specific errors:
{
"error": "Validation failed",
"details": {
"title": ["String must contain at least 1 character(s)"],
"start_date": ["Invalid date"]
}
}