ion } from 'express';
const router = express.Router();
// Path segment: identifies a specific inventory item
router.get('/v1/inventory/:sku', async (req: Request, res: Response) => {
const rawSku = req.params.sku;
// Strict validation: SKU must match alphanumeric pattern
if (!/^[A-Z0-9]{5,12}$/.test(rawSku)) {
return res.status(400).json({ error: 'Invalid SKU format' });
}
const inventoryItem = await fetchProductBySku(rawSku);
if (!inventoryItem) {
return res.status(404).json({ error: 'Inventory item not found' });
}
res.json(inventoryItem);
});
// Query modifier: filters and shapes a collection
router.get('/v1/inventory', async (req: Request, res: Response) => {
const filters = extractQueryFilters(req.query);
const results = await queryInventoryCollection(filters);
res.json({
data: results.items,
pagination: {
page: filters.page,
limit: filters.limit,
total: results.totalCount
}
});
});
### Step 2: Implement Type-Safe Query Extraction
Query values arrive as strings. Production systems require explicit coercion, defaulting, and sanitization to prevent type mismatches and injection vectors.
```typescript
interface InventoryFilters {
category: string | null;
minStock: number;
maxStock: number;
page: number;
limit: number;
sortBy: 'name' | 'price' | 'stock';
sortOrder: 'asc' | 'desc';
}
function extractQueryFilters(raw: Record<string, any>): InventoryFilters {
const safeParseInt = (val: any, fallback: number) => {
const parsed = parseInt(String(val), 10);
return isNaN(parsed) ? fallback : parsed;
};
return {
category: typeof raw.category === 'string' ? raw.category.trim() : null,
minStock: safeParseInt(raw.minStock, 0),
maxStock: safeParseInt(raw.maxStock, 10000),
page: Math.max(1, safeParseInt(raw.page, 1)),
limit: Math.min(100, Math.max(1, safeParseInt(raw.limit, 20))),
sortBy: ['name', 'price', 'stock'].includes(raw.sortBy) ? raw.sortBy : 'name',
sortOrder: ['asc', 'desc'].includes(raw.sortOrder) ? raw.sortOrder : 'asc'
};
}
Step 3: Compose Validation Middleware
Separate validation logic from route handlers. This enables reuse across endpoints and keeps handlers focused on orchestration.
function validatePathParams(requiredFields: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const missing = requiredFields.filter(field => !req.params[field]);
if (missing.length > 0) {
return res.status(400).json({ error: `Missing required path parameters: ${missing.join(', ')}` });
}
next();
};
}
function validateQueryTypes(schema: Record<string, 'string' | 'number' | 'boolean'>) {
return (req: Request, res: Response, next: NextFunction) => {
const errors: string[] = [];
for (const [key, expectedType] of Object.entries(schema)) {
if (req.query[key] !== undefined) {
const value = req.query[key];
if (expectedType === 'number' && isNaN(Number(value))) {
errors.push(`${key} must be a valid number`);
}
if (expectedType === 'boolean' && !['true', 'false'].includes(String(value).toLowerCase())) {
errors.push(`${key} must be true or false`);
}
}
}
if (errors.length > 0) {
return res.status(400).json({ error: 'Invalid query parameters', details: errors });
}
next();
};
}
// Apply middleware
router.get('/v1/inventory', validateQueryTypes({ page: 'number', limit: 'number', active: 'boolean' }), /* handler */);
router.get('/v1/inventory/:sku', validatePathParams(['sku']), /* handler */);
Architecture Decisions & Rationale
- Strict Path Validation: Path parameters are required by definition. Failing fast on malformed identifiers prevents downstream database queries and reduces attack surface.
- Query Coercion Layer: Query strings are inherently unstructured. Centralizing type conversion and boundary enforcement (e.g.,
limit capped at 100) prevents resource exhaustion and inconsistent responses.
- Middleware Composition: Validation logic is decoupled from business logic. This enables independent testing, easier OpenAPI generation, and consistent error formatting across the API surface.
- Response Shape Consistency: Collection endpoints return paginated envelopes. Single-resource endpoints return raw entities. This aligns with client expectations and simplifies state management in frontend applications.
Pitfall Guide
1. Treating Path Parameters as Optional
Explanation: Developers sometimes define routes like /api/items/:id? expecting Express to handle missing values gracefully. Express routing does not support optional path segments natively without regex workarounds, and doing so breaks REST semantics.
Fix: Split into two explicit routes: /api/items for collections and /api/items/:id for single resources. Use middleware to share business logic if needed.
2. Ignoring String Coercion in req.params
Explanation: All path parameters arrive as strings. Passing them directly to numeric database queries or mathematical operations causes silent type mismatches or query failures.
Fix: Always coerce immediately after extraction. Use parseInt() or Number() with explicit radix, and validate against expected ranges before downstream usage.
3. Overloading Query Strings for Resource Identification
Explanation: Using /api/users?id=42 instead of /api/users/42 breaks CDN caching, complicates bookmarking, and violates REST conventions. Proxies may strip or reorder query parameters, causing cache fragmentation.
Fix: Reserve query strings exclusively for filtering, sorting, pagination, and representation control. Move all resource identifiers into path segments.
Explanation: Query strings are user-controlled and often bypass input validation pipelines. Unsanitized values can trigger NoSQL injection, SQL injection, or excessive memory allocation during sorting/filtering.
Fix: Implement a strict allowlist for query keys. Sanitize string values, enforce numeric bounds, and reject unknown parameters with a 400 Bad Request response.
5. Inconsistent Naming Conventions Across Endpoints
Explanation: Mixing userId, user_id, and userUuid across routes forces client SDKs to maintain mapping tables and increases documentation drift.
Fix: Adopt a single naming strategy (e.g., camelCase for all parameters) and enforce it via linting rules or OpenAPI schema validation. Document the convention in the API style guide.
6. Forgetting to Handle Undefined Query Values
Explanation: Accessing req.query.sortBy without checking for undefined causes runtime errors when the client omits the parameter. Defaulting logic scattered across handlers creates maintenance debt.
Fix: Centralize query extraction in a dedicated utility function. Apply defaults at the extraction layer, not inside route handlers.
7. Mixing Identification and Filtering in the Same Segment
Explanation: Routes like /api/products/electronics/active conflate category identification with status filtering. This creates exponential route combinations and prevents flexible client queries.
Fix: Use /api/products?category=electronics&status=active. Path segments should represent hierarchical resource ownership, not attribute filtering.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Fetching a single entity by ID | Path Segment (/v1/orders/:orderId) | Enables aggressive CDN caching, aligns with REST, simplifies client routing | Low (standard implementation) |
| Filtering a collection by attributes | Query Modifier (/v1/orders?status=pending&date=2024-01) | Keeps route table flat, supports dynamic combinations, prevents exponential path growth | Medium (requires validation layer) |
| Nested resource ownership | Path Segment (/v1/users/:userId/posts/:postId) | Represents hierarchical relationship, maintains resource identity | Low |
| Pagination and sorting | Query Modifier (/v1/posts?page=2&sort=created) | Optional by nature, varies per request, does not change resource identity | Low |
| Feature toggles or representation control | Query Modifier (/v1/reports?format=csv&include=metadata) | Modifies response shape without altering resource location | Low |
| Multi-tenant routing | Path Segment (/v1/:tenantId/dashboard) | Tenant context is mandatory for resource resolution, affects security boundaries | Medium (requires auth middleware) |
Configuration Template
// src/middleware/routing-validators.ts
import { Request, Response, NextFunction } from 'express';
export function enforcePathParams(required: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const missing = required.filter(p => !req.params[p]?.trim());
if (missing.length) {
return res.status(400).json({ code: 'MISSING_PATH_PARAM', fields: missing });
}
next();
};
}
export function sanitizeQuery(allowedKeys: string[], typeMap: Record<string, 'string' | 'number' | 'boolean'>) {
return (req: Request, res: Response, next: NextFunction) => {
const unknown = Object.keys(req.query).filter(k => !allowedKeys.includes(k));
if (unknown.length) {
return res.status(400).json({ code: 'UNKNOWN_QUERY_PARAM', fields: unknown });
}
for (const [key, type] of Object.entries(typeMap)) {
const val = req.query[key];
if (val !== undefined) {
if (type === 'number' && isNaN(Number(val))) {
return res.status(400).json({ code: 'INVALID_QUERY_TYPE', field: key, expected: 'number' });
}
if (type === 'boolean' && !['true', 'false'].includes(String(val).toLowerCase())) {
return res.status(400).json({ code: 'INVALID_QUERY_TYPE', field: key, expected: 'boolean' });
}
}
}
next();
};
}
// src/routes/catalog.ts
import { Router } from 'express';
import { enforcePathParams, sanitizeQuery } from '../middleware/routing-validators';
const catalogRouter = Router();
catalogRouter.get(
'/v1/catalog',
sanitizeQuery(['category', 'minPrice', 'maxPrice', 'page', 'limit', 'sort'], {
minPrice: 'number', maxPrice: 'number', page: 'number', limit: 'number'
}),
async (req, res) => { /* collection handler */ }
);
catalogRouter.get(
'/v1/catalog/:sku',
enforcePathParams(['sku']),
async (req, res) => { /* single resource handler */ }
);
export default catalogRouter;
Quick Start Guide
- Initialize Router Structure: Create separate route files for collection endpoints and single-resource endpoints. Avoid mixing both in the same file to enforce semantic boundaries.
- Define Validation Middleware: Copy the
enforcePathParams and sanitizeQuery templates. Adjust allowed keys and type maps to match your domain model.
- Apply Middleware to Routes: Attach path validation to identifier routes and query sanitization to collection routes. Ensure handlers remain focused on data fetching and response formatting.
- Test Edge Cases: Send requests with missing path segments, malformed query values, unknown parameters, and boundary conditions. Verify that validation middleware returns consistent
400 responses before hitting business logic.
- Document the Contract: Update your OpenAPI specification to mark path parameters as
required: true and query parameters as required: false. Include examples demonstrating correct usage patterns for frontend and partner integrations.