26 KiB
Backend Routes Module Documentation
Overview
The Routes module defines all HTTP endpoints for the application. It uses a factory pattern for entity CRUD operations and custom routes for specialized functionality.
Directory: src/routes/
Files (25 total):
| File | Lines | Pattern | Description |
|---|---|---|---|
auth.ts |
327 | Custom | Authentication endpoints |
file.ts |
150 | Custom | File upload/download endpoints |
publish.ts |
107 | Custom | Publishing workflow endpoints |
search.ts |
64 | Custom | Global search endpoint |
runtime-context.ts |
16 | Custom | Runtime context inspection |
projects.ts |
46 | Hybrid | Projects CRUD + custom clone endpoint |
users.ts |
64 | Hybrid | Users CRUD via factory + sanitized GET by ID |
tour_pages.ts |
380 | Manual CRUD | Tour pages CRUD plus reorder, duplicate, reverse-video status |
roles.ts |
141 | Factory | Roles CRUD via factory |
permissions.ts |
188 | Factory | Permissions CRUD via factory |
assets.ts |
155 | Factory | Assets CRUD via factory |
asset_variants.ts |
147 | Factory | Asset variants CRUD via factory |
access_logs.ts |
145 | Factory | Access logs CRUD via factory |
project_memberships.ts |
145 | Factory | Project memberships CRUD via factory |
project_audio_tracks.ts |
151 | Factory | Project audio tracks CRUD via factory |
global_transition_defaults.ts |
145 | Custom | Runtime-readable global transition defaults |
global_ui_control_defaults.ts |
79 | Custom | Runtime-readable global UI-control defaults |
project_transition_settings.ts |
212 | Custom | Project/environment transition overrides |
project_ui_control_settings.ts |
125 | Custom | Project/environment global UI-control overrides |
presigned_url_requests.ts |
150 | Factory | Presigned URL requests CRUD via factory |
publish_events.ts |
157 | Factory | Publish events CRUD via factory |
pwa_caches.ts |
148 | Factory | PWA caches CRUD via factory |
element_type_defaults.ts |
12 | Factory | Element type defaults via factory |
project_element_defaults.ts |
92 | Hybrid | Project element defaults CRUD + custom (reset, diff) |
Architecture Diagram
┌──────────────────────────────────────────────────────────────────────┐
│ Route Patterns │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Factory Pattern (createEntityRouter) │ │
│ │ │ │
│ │ routes/roles.ts ──────────────┐ │ │
│ │ routes/permissions.ts ────────┤ │ │
│ │ routes/assets.ts ─────────────┼───▶ factories/router.factory.ts│ │
│ │ routes/asset_variants.ts ─────┤ │ │
│ │ routes/access_logs.ts ────────┤ Generates: │ │
│ │ routes/pwa_caches.ts ─────────┤ • POST / │ │
│ │ routes/publish_events.ts ─────┤ • POST /bulk-import │ │
│ │ routes/element_type_defaults.ts • PUT /:id │ │
│ │ routes/presigned_url_requests.ts • DELETE /:id │ │
│ │ routes/project_memberships.ts • POST /deleteByIds │ │
│ │ routes/project_audio_tracks.ts • GET / │ │
│ │ ... (11 entities) • GET /count │ │
│ │ • GET /autocomplete │ │
│ │ • GET /:id │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Hybrid Pattern (Factory + Custom Routes) │ │
│ │ │ │
│ │ routes/projects.ts ──────────────────┐ │ │
│ │ • Standard CRUD (manual) │ │ │
│ │ + POST /:id/clone │ │ │
│ │ + GET /:id/offline-manifest │ │ │
│ │ │ │ │
│ │ routes/project_element_defaults.ts ──┘ │ │
│ │ • Standard CRUD (factory) │ │
│ │ + POST /:id/reset │ │
│ │ + GET /:id/diff │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Custom Pattern (Full Manual Implementation) │ │
│ │ │ │
│ │ routes/auth.ts ───────── Authentication flows │ │
│ │ routes/file.ts ───────── File upload/download │ │
│ │ routes/publish.ts ────── Publishing workflow │ │
│ │ routes/search.ts ─────── Global search │ │
│ │ routes/runtime-context.ts ─ Runtime inspection │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Route Factory
factories/router.factory.ts
Generates standardized CRUD routes for entities.
const router = createEntityRouter(
'tour_pages', // Entity name
Tour_pagesService, // Service class
Tour_pagesDBApi, // Database API class
{
permissionEntity: 'tour_pages', // Permission entity name (optional)
csvFields: ['id', 'name'], // CSV export fields (optional)
validation: { // Request validation overrides (optional)
create: customCreateSchema,
update: customUpdateSchema,
},
customRoutes: (router, Service, DBApi) => { // Custom routes (optional)
router.post('/custom', handler);
}
}
);
Generated Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/ |
Create new item |
POST |
/bulk-import |
Bulk import items |
PUT |
/:id |
Update item by ID |
DELETE |
/:id |
Delete item by ID |
POST |
/deleteByIds |
Delete multiple items |
GET |
/ |
List items (with pagination, filters) |
GET |
/count |
Count items matching filters |
GET |
/autocomplete |
Autocomplete search |
GET |
/:id |
Get single item by ID |
Factory Features
Permission Checking:
router.use(checkCrudPermissions(permissionEntity));
// Maps HTTP methods to permissions:
// GET → READ_ENTITY
// POST → CREATE_ENTITY
// PUT/PATCH → UPDATE_ENTITY
// DELETE → DELETE_ENTITY
Request Validation:
import { validateRequest } from '../middlewares/validate-request.ts';
import { crud as crudSchemas } from '../validators/request-schemas.ts';
router.get(
'/:id',
validateRequest(crudSchemas.findOne),
wrapAsync(async (req, res) => {
const payload = await DBApi.findBy({ id: req.params.id });
res.status(200).send(payload);
}),
);
Factory CRUD routes validate request bodies, params, and common query controls before service/DB calls. List and count routes validate limit, page, field, sort, and filetype while keeping existing entity filter query parameters. Entity routers can pass validation overrides for stricter contracts, as users and projects do.
CSV Export:
// GET /?filetype=csv
if (filetype === 'csv') {
const csv = parse(payload.rows, { fields });
res.status(200).attachment('export.csv').send(csv);
}
Runtime Context:
const runtimeContext = req.runtimeContext;
const payload = await DBApi.findAll(req.query, {
currentUser,
runtimeContext,
});
Route Mounting (index.js)
Routes are mounted with authentication and rate limiting:
// No auth required
app.use('/api/auth', authRoutes);
app.use('/api/runtime-context', runtimeContextRoutes);
app.get('/api/health', healthHandler);
// File routes (before body parser)
app.use('/api/file/download', downloadLimiter);
app.use('/api/file/presign', downloadLimiter);
app.use('/api/file/upload', uploadLimiter);
app.use('/api/file', fileRoutes);
// JWT auth required
app.use('/api/users', jwtAuth, usersRoutes);
app.use('/api/roles', jwtAuth, rolesRoutes);
app.use('/api/permissions', jwtAuth, permissionsRoutes);
app.use('/api/project_memberships', jwtAuth, project_membershipsRoutes);
app.use('/api/assets', jwtAuth, assetsRoutes);
app.use('/api/asset_variants', jwtAuth, asset_variantsRoutes);
app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes);
app.use('/api/publish_events', jwtAuth, publish_eventsRoutes);
app.use('/api/pwa_caches', jwtAuth, pwa_cachesRoutes);
app.use('/api/access_logs', jwtAuth, access_logsRoutes);
app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes);
app.use('/api/project-element-defaults', jwtAuth, project_element_defaultsRoutes);
app.use('/api/publish', jwtAuth, publishRoutes);
// JWT + Rate limiting
app.use('/api/search', jwtAuth, searchLimiter, searchRoutes);
// Runtime public access (production environment)
mountRuntimeEntityRoute('/api/projects', 'projects', projectsRoutes);
mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes);
mountRuntimeEntityRoute('/api/project_audio_tracks', 'project_audio_tracks', ...);
Custom Routes Detail
1. auth.ts (327 lines)
Authentication and account management.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /signin/local |
No | Email/password login |
| POST | /signup |
No | Register new user |
| GET | /me |
JWT | Get current user |
| PUT | /password-reset |
No | Reset password with token |
| PUT | /password-update |
JWT | Change password |
| PUT | /profile |
JWT | Update user profile |
| PUT | /verify-email |
No | Verify email with token |
| POST | /send-email-address-verification-email |
JWT | Resend verification |
| POST | /send-password-reset-email |
No | Send reset email |
| GET | /email-configured |
No | Check email config |
| GET | /signin/google |
No | Google OAuth start |
| GET | /signin/google/callback |
No | Google OAuth callback |
| GET | /signin/microsoft |
No | Microsoft OAuth start |
| GET | /signin/microsoft/callback |
No | Microsoft OAuth callback |
auth.ts is a typed ESM route boundary. It uses reusable request body/query contracts from backend/src/types/auth-routes.ts, the typed backend/src/services/auth.ts service, and the shared Passport helpers in backend/src/auth/passport-middleware.ts.
2. file.ts (150 lines)
File upload and download operations.
file.ts is a typed ESM route boundary. It calls the typed unified storage facade in backend/src/services/file.ts, while provider contracts and request/response shapes live in reusable backend/src/types/file.ts contracts. S3 and GCloud use official SDK-provided types instead of local SDK declarations.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /download |
No | Download file by privateUrl |
| POST | /presign |
No | Generate presigned URLs (max 50) |
| POST | /upload/:table/:field |
JWT | Legacy single file upload |
| POST | /upload-sessions/init |
JWT | Initialize chunked upload |
| GET | /upload-sessions/:sessionId |
JWT | Get upload session status |
| PUT | /upload-sessions/:sessionId/chunks/:chunkIndex |
JWT | Upload chunk |
| POST | /upload-sessions/:sessionId/finalize |
JWT | Finalize chunked upload |
Presigned URLs Request:
{
"urls": ["assets/image.jpg", "assets/video.mp4"]
}
Presigned URLs Response:
{
"presignedUrls": {
"assets/image.jpg": "https://s3.../assets/image.jpg?X-Amz-...",
"assets/video.mp4": "https://s3.../assets/video.mp4?X-Amz-..."
}
}
3. publish.ts (107 lines)
Publishing workflow for Dev → Stage → Production.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | / |
JWT | Publish stage to production |
| POST | /publish |
JWT | Alias for publish |
| POST | /save-to-stage |
JWT | Save dev to stage |
Publish Request:
{
"projectId": "uuid",
"title": "Release 1.0",
"description": "Initial release"
}
Save to Stage Request:
{
"projectId": "uuid"
}
4. search.ts (64 lines)
Global full-text search across entities.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | / |
JWT | Search across all entities |
Request:
{
"searchQuery": "my search term"
}
Response:
{
"users": [...],
"projects": [...],
"tour_pages": [...]
}
5. runtime-context.ts (16 lines)
Runtime context inspection for debugging.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
No | Get current runtime context |
Response:
{
"mode": "admin",
"projectSlug": null,
"headerEnvironment": "production",
"headerProjectSlug": "my-tour"
}
Entity Routes Detail
projects.ts (46 lines)
Projects with factory CRUD plus a clone endpoint.
Standard CRUD Endpoints:
| Method | Path | Description |
|---|---|---|
| POST | / |
Create project |
| POST | /bulk-import |
Bulk import projects |
| PUT | /:id |
Update project |
| DELETE | /:id |
Delete project |
| POST | /deleteByIds |
Delete multiple projects |
| GET | / |
List projects |
| GET | /count |
Count projects |
| GET | /autocomplete |
Autocomplete search |
| GET | /:id |
Get project by ID |
| Method | Path | Description |
|---|---|---|
| POST | /:id/clone |
Clone project with all pages |
Custom Endpoints:
tour_pages.ts (380 lines)
Typed manual CRUD route for tour pages. In addition to standard create/update/delete/list/count/autocomplete/find-by-id operations, it owns page reorder, dev-page duplication, CSV export, and reverse-video status endpoints. The route uses reusable contracts from backend/src/types/tour-pages.ts.
Schema Fields:
source_key,name,slugbackground_image_url,background_video_url,background_audio_urlui_schema_json,sort_order
Endpoints:
| Method | Path | Description |
|---|---|---|
| POST | / |
Create page |
| POST | /bulk-import |
Bulk import pages |
| POST | /reorder |
Reorder dev pages in a project/environment |
| POST | /:id/duplicate |
Duplicate a dev page |
| PUT | /:id |
Update page |
| DELETE | /:id |
Remove page |
| POST | /deleteByIds |
Remove multiple pages |
| GET | / |
List pages with reverse video URL population and optional CSV export |
| POST | /reverse-video-status |
Check generated reverse-video variants |
| GET | /count |
Count pages |
| GET | /autocomplete |
Page autocomplete |
| GET | /:id |
Get one page with reverse video URL population |
POST /api/tour_pages/reorderaccepts{ data: { projectId, environment, orderedPageIds } }, validates a complete page list for the project/dev environment, and updates onlysort_order.POST /api/tour_pages/:id/duplicateaccepts{ data: { projectId, environment, name, slug } }, duplicates a dev page into a new independent dev page, appends it to the project order, deep-copiesui_schema_json, and regenerates inline element IDs.- Constructor page deletion uses the standard
DELETE /api/tour_pages/:idroute after a frontend confirmation modal; no separate delete endpoint is needed. - Custom routes are protected by Passport JWT like other tour page mutations and
are implemented before
/:idwhere needed so custom paths are not treated as entity IDs. - Non-dev environments are rejected for reorder and duplicate; stage and production changes only flow through Save to Stage and Publish.
element_type_defaults.ts (12 lines)
Minimal factory usage with permission override.
module.exports = createEntityRouter(
'element_type_defaults',
Element_type_defaultsService,
Element_type_defaultsDBApi,
{
permissionEntity: 'page_elements', // Uses PAGE_ELEMENTS permissions
},
);
URL Aliases:
/api/element-type-defaults(primary)/api/ui-elements(backwards compatibility)
project_element_defaults.ts (92 lines)
Factory-generated routes plus custom endpoints for resetting and comparing defaults.
const baseRouter = createEntityRouter(
'project_element_defaults',
Project_element_defaultsService,
Project_element_defaultsDBApi,
{
permissionEntity: 'page_elements',
},
);
// Add custom endpoints
baseRouter.post('/:id/reset', ...);
baseRouter.get('/:id/diff', ...);
Standard CRUD Endpoints: (via factory)
| Method | Path | Description |
|---|---|---|
| POST | / |
Create project element default |
| PUT | /:id |
Update project element default |
| DELETE | /:id |
Delete project element default |
| GET | / |
List project element defaults |
| GET | /:id |
Get project element default by ID |
Custom Endpoints:
| Method | Path | Description |
|---|---|---|
| POST | /:id/reset |
Reset project element default to global |
| GET | /:id/diff |
Get diff from global element type default |
URL Alias:
/api/project-element-defaults(primary)
Request/Response Patterns
List Endpoint (GET /)
Query Parameters:
| Param | Type | Description |
|---|---|---|
page |
number | Page number (0-indexed) |
limit |
number | Items per page |
field |
string | Sort field |
sort |
string | Sort direction (asc/desc) |
filetype |
string | Export format (csv) |
[fieldName] |
string | Filter by field value |
[fieldName]Range |
array | Filter by range [start, end] |
Response:
{
"rows": [...],
"count": 150
}
Create Endpoint (POST /)
Request:
{
"data": {
"name": "New Item",
"field1": "value1"
}
}
Response:
{
"id": "uuid",
"name": "New Item",
"field1": "value1",
"createdAt": "2024-01-01T00:00:00.000Z"
}
Update Endpoint (PUT /:id)
Request:
{
"id": "uuid",
"data": {
"name": "Updated Name"
}
}
Response:
true
Delete Endpoints
Single Delete (DELETE /:id):
true
Bulk Delete (POST /deleteByIds):
{
"data": ["uuid1", "uuid2", "uuid3"]
}
Swagger Documentation
Swagger UI is served at GET /api-docs. The OpenAPI document is maintained in
backend/src/openapi/document.ts, not in route-local JSDoc comments.
const specs = createOpenApiDocument({
serverUrl: config.server.swaggerServerUrl,
});
The OpenAPI module contains:
- Shared schemas for auth, users, projects, tour pages, assets, publishing, runtime access, file upload, UI controls, transition settings, and audit entities.
- A reusable factory CRUD path generator covering
POST /,POST /bulk-import,PUT /:id,DELETE /:id,POST /deleteByIds,GET /,GET /count,GET /autocomplete, andGET /:id. - Explicit path definitions for custom routes such as auth flows, publishing, file upload/download, runtime access/context, project clone, tour page reorder and duplicate, transition/UI-control settings, and project element default reset/diff.
When adding routes, update backend/src/openapi/document.ts alongside the route
implementation. For factory-backed entities, add a schema and CrudResource
entry instead of copying per-route Swagger boilerplate.
Access Swagger UI: http://localhost:3000/api-docs in local development.
On the standard VM, the backend runs in NODE_ENV=dev_stage on port 3000;
Apache serves the public domain on port 80. See
deployment-vm.md for VM health checks.
Error Handling
wrapAsync Helper
All async route handlers use wrapAsync for error propagation:
router.post(
'/',
wrapAsync(async (req, res) => {
// Errors thrown here are caught and passed to error handler
const payload = await Service.create({
data: req.body.data,
currentUser: req.currentUser,
runtimeContext: req.runtimeContext,
});
res.status(200).send(payload);
}),
);
commonErrorHandler
Each route file includes error handler:
router.use('/', require('../helpers').commonErrorHandler);
Error Response Mapping:
| Status Code | Description |
|---|---|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 422 | Unprocessable Entity |
| 500 | Internal Server Error |
Route Categories
Public Routes (No Auth)
| Route | Description |
|---|---|
GET /api/health |
Health check |
POST /api/auth/signin/local |
Login |
GET /api/auth/signin/google |
Google OAuth |
GET /api/auth/signin/microsoft |
Microsoft OAuth |
GET /api/file/download |
File download |
POST /api/file/presign |
Generate presigned URLs |
GET /api/runtime-context |
Runtime context |
Runtime Public Routes (Production Environment)
| Route | Description |
|---|---|
GET /api/projects |
List projects (sanitized) |
GET /api/tour_pages |
List tour pages (sanitized) |
GET /api/project_audio_tracks |
List audio tracks (sanitized) |
Authenticated Routes (JWT Required)
All other routes require JWT authentication via:
app.use('/api/users', jwtAuth, usersRoutes);
Rate Limited Routes
| Route | Limiter | Config |
|---|---|---|
/api/auth/signin/local |
authLimiter | 10/15min |
/api/auth/send-password-reset-email |
passwordResetLimiter | 5/hour |
/api/file/upload* |
uploadLimiter | 10/min |
/api/file/download, /presign |
downloadLimiter | 200/min |
/api/search |
searchLimiter | 30/min |
Dependencies
| Package | Purpose |
|---|---|
express |
Router and middleware |
json2csv |
CSV export functionality |
passport |
JWT authentication |
body-parser |
JSON body parsing |
Testing
Test Entity CRUD
# Create
curl -X POST http://localhost:3000/api/projects \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"data": {"name": "Test Project", "slug": "test"}}'
# List
curl http://localhost:3000/api/projects \
-H "Authorization: Bearer <JWT>"
# Get by ID
curl http://localhost:3000/api/projects/<uuid> \
-H "Authorization: Bearer <JWT>"
# Update
curl -X PUT http://localhost:3000/api/projects/<uuid> \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"id": "<uuid>", "data": {"name": "Updated Name"}}'
# Delete
curl -X DELETE http://localhost:3000/api/projects/<uuid> \
-H "Authorization: Bearer <JWT>"
Test Public Runtime
curl http://localhost:3000/api/projects \
-H "X-Runtime-Environment: production"
Test Presigned URLs
curl -X POST http://localhost:3000/api/file/presign \
-H "Content-Type: application/json" \
-d '{"urls": ["assets/test.jpg"]}'
Summary
The Routes module provides:
- Factory Pattern - 12 entities with standardized CRUD (createEntityRouter)
- Hybrid Pattern - 2 entities with factory CRUD + custom endpoints (projects, project_element_defaults)
- Custom Routes - 7 specialized route files for auth, files, search, etc.
- Swagger Documentation - centralized OpenAPI document for all endpoints
- Permission Checking - RBAC via checkCrudPermissions middleware
- Runtime Public Access - Production environment public read access
- Rate Limiting - Per-endpoint rate limiting
- CSV Export - Export capability via
?filetype=csv - Request Validation - Joi schemas via validateRequest before service calls
- Error Handling - Centralized via wrapAsync and commonErrorHandler
Route Statistics:
- 26 route files
- ~50+ unique endpoints
- 11 factory-generated entity routers
- 3 hybrid entity routers (factory + custom)
- 7 custom route implementations