API Conventions
URL structure
Section titled “URL structure”/api/{resource} ← collection/api/{resource}/:id ← single item/api/{resource}/:id/{action} ← action on item/api/{resource}/public/{sub} ← unauthenticated sub-resourceExamples:
GET /api/tickets ← list ticketsGET /api/tickets/:id ← single ticketPOST /api/tickets/:id/book ← action: book ticketGET /api/tickets/search ← public search (no auth)GET /api/submissions/public/search/:id ← public status lookupWatch out: Express matches routes in declaration order. Routes like
/search,/destinations, and/public/search/:idmust be declared before/:idin the router file. If/:idcomes first, Express will try to decrypt “search” as a ticket ID and return 404.
HTTP methods
Section titled “HTTP methods”| Method | Use for | Success code |
|---|---|---|
GET | Read-only fetches | 200 |
POST | Creating resources or triggering actions | 201 (create) / 200 (action) |
PUT | Updating a resource | 200 |
DELETE | Removing a resource | 200 |
Response format
Section titled “Response format”Success
Section titled “Success”{ "message": "Submission created successfully", "id": 42 }For list endpoints:
[{ "id": 1, "name": "John", ... }, ...]For single resource:
{ "id": 1, "name": "John", ... }{ "error": "Description of what went wrong" }For validation errors (using express-validator):
{ "errors": [{ "msg": "Name is required", "path": "name" }] }Rules:
- Never return raw database error messages to the client (
err.message) — log them server-side and return a generic message - Use consistent HTTP status codes:
400— bad request / validation failure401— not authenticated (no/expired token)403— authenticated but not authorized404— resource not found409— conflict (e.g., duplicate, stale state)500— unexpected server error
Authentication header
Section titled “Authentication header”All protected routes expect:
Authorization: Bearer <access_token>Never accept tokens from query strings (?token=...) — the current middleware already enforces this.
Input validation
Section titled “Input validation”Two validation approaches are used in the codebase:
1. express-validator (submissions, structured forms)
Section titled “1. express-validator (submissions, structured forms)”const { body, validationResult } = require('express-validator');
const rules = [ body('email').isEmail().normalizeEmail(), body('name').trim().escape().notEmpty(),];
router.post('/', rules, (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); // proceed});2. Manual sanitization (auth routes)
Section titled “2. Manual sanitization (auth routes)”const sanitizeString = (str) => { if (typeof str !== 'string') return ''; return str.replace(/[<>'";\\]/g, '').trim().substring(0, 255);};Recommendation: Standardize on express-validator for all new routes. The manual sanitizeString approach in auth.js is not wrong, but mixing two validation styles makes the codebase harder to audit.
File uploads
Section titled “File uploads”Routes that accept files use multer middleware:
const upload = require('../config/multer');
// Single filerouter.post('/route', upload.single('fieldName'), handler);
// Multiple named fieldsrouter.post('/route', upload.fields([ { name: 'passportPhoto', maxCount: 10 }, { name: 'receiptPdf', maxCount: 10 }]), handler);
// Any fieldrouter.post('/route', upload.any(), handler);Files come as req.file (single) or req.files (multiple). Always pass files through saveFile() in utils/storage.js.
Role enforcement pattern
Section titled “Role enforcement pattern”For endpoints that need DB-fresh role checks (not JWT-cached):
router.put('/:id', authenticateToken, (req, res) => { // Re-fetch from DB to prevent stale-token attacks db.get('SELECT role, agentType FROM users WHERE id = ?', [req.user.id], (err, dbUser) => { if (err || !dbUser) return res.status(401).json({ error: 'User verification failed' });
const role = dbUser.role; // use this const agentType = dbUser.agentType; // use this // NOT req.user.role });});Use this pattern on any endpoint that changes data based on the user’s role.
Pagination
Section titled “Pagination”The codebase currently does no pagination — all list endpoints return the full dataset. This will become a performance issue as data grows. When adding a feature that could have many records (e.g., bookings, submissions), add cursor or offset pagination from the start:
const { limit = 50, offset = 0 } = req.query;db.all('SELECT * FROM table ORDER BY createdAt DESC LIMIT ? OFFSET ?', [parseInt(limit), parseInt(offset)], callback);Rate limiting
Section titled “Rate limiting”The express-rate-limit package is installed but not applied anywhere. At minimum, add rate limiting to auth routes:
// In backend/index.js, before route registrationconst rateLimit = require('express-rate-limit');const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 });app.use('/api/auth/login', authLimiter);app.use('/api/auth/register', authLimiter);