Resource-Based URLs
Http
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
Nested Resources
Http
GET /users/123/orders
GET /users/123/orders/456
POST /users/123/orders
GET /orders/456/items
POST /orders/456/items
HTTP methods and status codes
Tsx
// Success
200 OK // GET, PUT, PATCH
201 Created // POST
204 No Content // DELETE
// Client errors
400 Bad Request // Malformed request
401 Unauthorized // Not authenticated
403 Forbidden // Not allowed
404 Not Found // Resource missing
409 Conflict // e.g. duplicate, version conflict
422 Unprocessable Entity // Validation failed
// Server errors
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
Tsx
app.get('/users', async (req, res) => {
const users = await userService.getAllUsers();
res.json(users);
});
app.post('/users', async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.put('/users/:id', async (req, res) => {
try {
const user = await userService.updateUser(req.params.id, req.body);
res.json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Consistent Response and Error Format
Tsx
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
message: string;
code: string;
details?: any;
};
meta?: {
pagination?: {
page: number;
limit: number;
total: number;
totalPages: number;
};
timestamp: string;
};
}
function sendSuccess<T>(res: Response, data: T, statusCode = 200) {
const response: ApiResponse<T> = {
success: true,
data,
meta: {
timestamp: new Date().toISOString()
}
};
res.status(statusCode).json(response);
}
Tsx
app.get('/users', async (req, res) => {
try {
const { page = 1, limit = 10, sort = 'id', order = 'asc' } = req.query;
const paginationParams = {
page: parseInt(page as string),
limit: parseInt(limit as string),
sort: sort as string,
order: order as 'asc' | 'desc'
};
const result = await userService.getUsersPaginated(paginationParams);
const response = {
data: result.users,
pagination: {
page: paginationParams.page,
limit: paginationParams.limit,
total: result.total,
totalPages: Math.ceil(result.total / paginationParams.limit),
hasNext: paginationParams.page < Math.ceil(result.total / paginationParams.limit),
hasPrev: paginationParams.page > 1
}
};
sendSuccess(res, response);
} catch (error) {
sendError(res, 'Failed to fetch users', 'FETCH_ERROR', 500);
}
});
Error Handling
Tsx
enum ErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
CONFLICT = 'CONFLICT',
INTERNAL_ERROR = 'INTERNAL_ERROR'
}
class ApiError extends Error {
constructor(
public message: string,
public code: ErrorCode,
public statusCode: number = 400,
public details?: any
) {
super(message);
this.name = 'ApiError';
}
}
Auth Middleware
Tsx
function authenticateToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET!, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user as any;
next();
});
}