Skip to content

API Conventions

/api/{resource} ← collection
/api/{resource}/:id ← single item
/api/{resource}/:id/{action} ← action on item
/api/{resource}/public/{sub} ← unauthenticated sub-resource

Examples:

GET /api/tickets ← list tickets
GET /api/tickets/:id ← single ticket
POST /api/tickets/:id/book ← action: book ticket
GET /api/tickets/search ← public search (no auth)
GET /api/submissions/public/search/:id ← public status lookup

Watch out: Express matches routes in declaration order. Routes like /search, /destinations, and /public/search/:id must be declared before /:id in the router file. If /:id comes first, Express will try to decrypt “search” as a ticket ID and return 404.

MethodUse forSuccess code
GETRead-only fetches200
POSTCreating resources or triggering actions201 (create) / 200 (action)
PUTUpdating a resource200
DELETERemoving a resource200
{ "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 failure
    • 401 — not authenticated (no/expired token)
    • 403 — authenticated but not authorized
    • 404 — resource not found
    • 409 — conflict (e.g., duplicate, stale state)
    • 500 — unexpected server error

All protected routes expect:

Authorization: Bearer <access_token>

Never accept tokens from query strings (?token=...) — the current middleware already enforces this.

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
});
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.

Routes that accept files use multer middleware:

const upload = require('../config/multer');
// Single file
router.post('/route', upload.single('fieldName'), handler);
// Multiple named fields
router.post('/route', upload.fields([
{ name: 'passportPhoto', maxCount: 10 },
{ name: 'receiptPdf', maxCount: 10 }
]), handler);
// Any field
router.post('/route', upload.any(), handler);

Files come as req.file (single) or req.files (multiple). Always pass files through saveFile() in utils/storage.js.

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.

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);

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 registration
const 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);