Directory Architecture
Overall Layout
arrange/
โโโ heineken-interview-arrange-backend/ # Bun + Hono REST API
โโโ heineken-interview-arrange-frontend/ # Next.js App Router admin console
โโโ heineken-interview-arrange-docs/ # This documentation (MkDocs)
The two applications are coupled only through OpenAPI: the backend generates /openapi.json from its route definitions, and the frontend generates React Query hooks and types from it with Orval.
Backend
heineken-interview-arrange-backend/
โโโ src/
โ โโโ app.ts # Hono app assembly (CORS, OpenAPI, Swagger UI, 404/error)
โ โโโ server.ts # Entry point
โ โโโ error.ts # Domain-specific AppError subclasses
โ โโโ routes/
โ โ โโโ index.ts
โ โ โโโ v1/ # HTTP layer; createRoute() pairs an OpenAPI definition with its handler
โ โโโ schemas/ # Zod schemas for requests/responses (public fields are snake_case)
โ โโโ services/ # Business logic
โ โโโ repositories/ # Drizzle queries only
โ โโโ db/
โ โ โโโ schema.ts # Single source of truth for all tables, enums and domain types
โ โโโ lib/ # response / errors / logger / pagination / date / databricks-client, etc.
โ โโโ middlewares/ # protected-route / error-handler / request-id / winston-logger
โ โโโ config/ # env / database / google / business-hours
โ โโโ types/ # Import related types
โ โโโ batch/ # Batches that run independently of the API (import / databricks-meta-sync / pii-cleanup)
โโโ __tests__/
โ โโโ unit/services/ # Vitest unit tests
โโโ scripts/ # setup.sh / migrate.ts / seed.ts
โโโ drizzle.config.ts
โโโ docker-compose.yml # PostgreSQL 17
โโโ Dockerfile # App (Lambda + aws-lambda-adapter)
โโโ Dockerfile.migration # Migration Lambda
โโโ Dockerfile.sync # Sync batch (ECS Fargate)
Layering
A one-way dependency: routes โ services โ repositories โ Drizzle.
| Directory | Responsibility | What it must not do |
|---|---|---|
src/routes/v1/ |
HTTP layer: OpenAPI definitions, auth and role control, snake_case โ camelCase conversion | Contain business logic or call Drizzle directly |
src/schemas/ |
Zod schemas. Public API fields are snake_case | Expose raw DB column names |
src/services/ |
Business logic, re-exported as namespaces from services/index.ts via export * as xxxService |
Depend on Hono's Context |
src/repositories/ |
Drizzle queries only; failures wrapped in DatabaseQueryError and friends |
Make business decisions |
src/db/schema.ts |
Single source of truth for tables, enums and $inferSelect / $inferInsert types |
Be split across multiple files |
ESM extensions
Imports must carry the .js extension (from '../lib/index.js'), even when pointing at TypeScript sources.
Where auth and roles are applied
Per router, never in individual handlers.
candidatesRoutes.use('*', protectedRoute());
candidatesRoutes.on(['POST', 'PATCH', 'PUT', 'DELETE'], ['/*'], requireRole('member'));
Frontend
heineken-interview-arrange-frontend/
โโโ src/
โ โโโ app/
โ โ โโโ (dashboard)/ # Authenticated admin console
โ โ โ โโโ page.tsx # Dashboard
โ โ โ โโโ projects/ # Project list, creation, detail (arrange flow)
โ โ โ โโโ surveys/ # Survey browsing
โ โ โโโ login/ # Login
โ โ โโโ scheduling/[token]/ # Public candidate page (unauthenticated)
โ โโโ api/ # Orval output. Never edit by hand
โ โ โโโ custom-fetch.ts # Adds Bearer token, returns { data, status, headers }
โ โ โโโ endpoints/ # Hooks grouped by tag
โ โ โโโ models/ # Type definitions
โ โโโ components/
โ โ โโโ ui/ # Generic UI (button / table / modal / slot-calendar, etc.)
โ โ โโโ layout/ # app-layout / header / sidebar
โ โ โโโ projects/ # arrange-steps / step1-form / dashboard-stats
โ โ โโโ step2/ # segment-sidebar / candidate-table / question-filter
โ โ โโโ candidates/ # candidate-row / summary-bar / status-helpers
โ โ โโโ providers.tsx # Shared providers such as React Query
โ โโโ lib/ # auth / utils / candidate-status / response-filter
โ โโโ types/
โ โโโ arrange-settings.ts # Arrange settings type, duplicated with the backend
โโโ orval.config.ts
โโโ next.config.ts
โโโ Dockerfile
Directory notes
| Directory | Description |
|---|---|
src/app/(dashboard)/ |
Authenticated console. A route group, so it does not appear in the URL |
src/app/scheduling/[token]/ |
Public page reachable with a token only; no authentication |
src/api/ |
Orval output. Named after paths, e.g. useGetApiV1ProjectsProjectId |
src/components/ui/ |
Reusable UI components |
src/lib/ |
The single gateway for auth tokens (auth.ts) plus utilities |
src/types/ |
Types not expressible via OpenAPI (JSON column contents, etc.) |
Reading API responses
custom-fetch.ts returns { data, status, headers }, and that data is the API envelope ({ success, data, ... }), so screens end up traversing response.data.data. Use the apiData() helper in lib/utils.ts.
const { data } = useGetApiV1Projects();
const projects = apiData<GetApiV1Projects200>(data);
// projects?.data โ Project[]
Where to Put New Files
Backend
| What you are adding | Where it goes |
|---|---|
| A new endpoint | src/routes/v1/<resource>.ts (extend an existing router, or create one and register it in routes/v1/index.ts) |
| Request/response definitions | src/schemas/<resource>.ts |
| Business logic | src/services/<resource>.ts, re-exported from services/index.ts |
| DB queries | src/repositories/<resource>.ts |
| Tables, enums, domain types | src/db/schema.ts (single file) |
| Environment variables | Both src/config/env.ts and .env.example |
| Generic errors | src/lib/errors.ts |
| Domain-specific errors | src/error.ts |
| Batches | src/batch/<name>.ts |
Frontend
| What you are adding | Where it goes |
|---|---|
| A screen | src/app/(dashboard)/**/page.tsx |
| Generic UI component | src/components/ui/ |
| Screen-specific component | src/components/<screen>/ |
| API calls | Do not add them โ regenerate src/api/** with bun run generate-api |
| Type definitions | src/types/ |