RESTful API design is the practice of structuring web APIs around resources, standard HTTP methods, and predictable conventions so that clients can create, read, update, and delete data over HTTP in a consistent way. A well-designed REST API uses plural nouns in URLs (such as /api/v1/users), maps HTTP verbs to actions (GET for reads, POST for creation, PUT and PATCH for updates, DELETE for removal), and returns accurate status codes like 200, 201, 404, and 422. Beyond naming, professional API design covers versioning through the URL path, JWT-based authentication with role-based authorization, rate limiting, request validation, and a uniform JSON response envelope for both success and error payloads. These conventions matter because APIs are contracts: consistent, intuitive patterns improve developer experience, reduce integration bugs, and cut support overhead. This guide walks through each practice with TypeScript and Express examples.
REST Fundamentals
HTTP Methods
Standard HTTP methods and their purposes:
- GET: Read resources (idempotent, safe)
- POST: Create new resources
- PUT: Full resource update (idempotent)
- PATCH: Partial resource update
- DELETE: Remove resources (idempotent)
HTTP Status Codes
2xx Success codes:
- 200 OK - General success
- 201 Created - Resource created successfully
- 204 No Content - Success with no response body
4xx Client error codes:
- 400 Bad Request - Invalid request format
- 401 Unauthorized - Authentication required
- 403 Forbidden - Insufficient permissions
- 404 Not Found - Resource not found
- 422 Unprocessable Entity - Validation error
How Should You Design Endpoints?
# Good - Plural nouns, kebab-case
GET /api/v1/users
GET /api/v1/users/123
POST /api/v1/users
PUT /api/v1/users/123
DELETE /api/v1/users/123
GET /api/v1/users/123/orders
GET /api/v1/order-items
# Bad - Avoid verbs in URLs
GET /api/v1/getUsers
POST /api/v1/createUser
DELETE /api/v1/deleteUser/123Filtering, Sorting, Pagination
# Filtering
GET /api/v1/products?category=electronics&price_min=100&price_max=500
# Sorting
GET /api/v1/products?sort=price&order=desc
GET /api/v1/products?sort=-price,+name
# Pagination
GET /api/v1/products?page=2&limit=20
# Combined
GET /api/v1/products?category=electronics&sort=-price&page=1&limit=20What Should Your Response Format Look Like?
Success Response
// Single resource
{
"data": {
"id": "123",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
}
}
// List with pagination
{
"data": [
{ "id": "1", "name": "Product 1" },
{ "id": "2", "name": "Product 2" }
],
"meta": {
"total": 100,
"page": 1,
"limit": 20,
"totalPages": 5
},
"links": {
"self": "/api/v1/products?page=1",
"next": "/api/v1/products?page=2",
"first": "/api/v1/products?page=1",
"last": "/api/v1/products?page=5"
}
}Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"code": "MIN_LENGTH"
}
],
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}TypeScript Interfaces
interface ApiResponse<T> {
data: T;
meta?: PaginationMeta;
links?: PaginationLinks;
}
interface ApiError {
error: {
code: string;
message: string;
details?: ErrorDetail[];
timestamp: string;
requestId: string;
};
}
interface PaginationMeta {
total: number;
page: number;
limit: number;
totalPages: number;
}
// Usage
type GetUsersResponse = ApiResponse<User[]>;
type GetUserResponse = ApiResponse<User>;How Should You Version an API?
URL versioning is the most common and recommended approach. It makes the API version explicit and easy to understand.
// Deprecation headers for old versions
app.use('/api/v1', (req, res, next) => {
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
res.set('Link', '</api/v2>; rel="successor-version"');
next();
});Authentication & Authorization
JWT Authentication
import jwt from 'jsonwebtoken';
async function authMiddleware(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Missing or invalid authorization header',
},
});
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Token is invalid or expired',
},
});
}
}Role-Based Access Control
function authorize(...allowedRoles: UserRole[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
error: { code: 'UNAUTHORIZED', message: 'Not authenticated' },
});
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' },
});
}
next();
};
}
// Usage
app.delete('/api/v1/users/:id', authMiddleware, authorize('admin'), deleteUser);Rate Limiting
import rateLimit from 'express-rate-limit';
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later',
},
},
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', generalLimiter);Validation with Zod
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
role: z.enum(['user', 'admin']).default('user'),
});
type CreateUserInput = z.infer<typeof createUserSchema>;
function validate<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: result.error.errors.map((err) => ({
field: err.path.join('.'),
message: err.message,
})),
},
});
}
req.body = result.data;
next();
};
}
app.post('/api/v1/users', validate(createUserSchema), createUser);A well-designed API should be consistent, intuitive, secure, performant, and evolvable. Apply these principles to create an excellent experience for developers using your API.
Remember that API design is about communication. Clear, consistent patterns make your API a joy to work with and reduce support overhead significantly.
