Shortcuts
BlogDecember 1, 2024

Mohamed Elbarry
RESTful API Design Principles
REST (Representational State Transfer) is an architectural style that defines a set of constraints for creating web services. The key principles are stateless, client-server, cacheable, uniform interface, layered system, and code on demand.
URL Design
# Good: Resource-based URLs
GET    /users
GET    /users/123
POST   /users
PUT    /users/123
DELETE /users/123

# Bad: Action-based URLs

GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
Hierarchical Resources
# Good: Hierarchical structure
GET    /users/123/orders
GET    /users/123/orders/456
POST   /users/123/orders
PUT    /users/123/orders/456
DELETE /users/123/orders/456

# For sub-resources that don't belong to a parent

GET /orders/456/items
POST /orders/456/items
HTTP Methods
// GET - Retrieve resources
app.get('/users', async (req, res) => {
const users = await userService.getAllUsers();
res.json(users);
});

// POST - Create new resources
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 });
}
});

// PUT - Update entire resource
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 });
}
});
Status Codes
// Success responses
200 OK          // Successful GET, PUT, PATCH
201 Created     // Successful POST
204 No Content  // Successful DELETE

// Client error responses
400 Bad Request // Invalid request data
401 Unauthorized // Authentication required
403 Forbidden // Insufficient permissions
404 Not Found // Resource doesn't exist
409 Conflict // Resource conflict
422 Unprocessable Entity // Validation errors

// Server error responses
500 Internal Server Error // Server error
502 Bad Gateway // Upstream server error
503 Service Unavailable // Service temporarily unavailable
Response Format
// Standard response wrapper
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;
};
}

// Success response helper
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);
}
Pagination
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
// Error types
enum ErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
NOT_FOUND = 'NOT_FOUND',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
CONFLICT = 'CONFLICT',
INTERNAL_ERROR = 'INTERNAL_ERROR'
}

// Custom error class
class ApiError extends Error {
constructor(
public message: string,
public code: ErrorCode,
public statusCode: number = 400,
public details?: any
) {
super(message);
this.name = 'ApiError';
}
}
Authentication
// JWT authentication middleware
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();
});
}
  • Use resource-based URLs - Make your API intuitive
  • Leverage HTTP methods and status codes - Use the protocol correctly
  • Maintain consistent response formats - Make responses predictable
  • Implement proper error handling - Help developers debug issues
  • Version your API - Plan for evolution
  • Secure your endpoints - Protect your data and users
  • Document everything - Make your API discoverable
  • Test thoroughly - Ensure reliability
A well-designed API is a joy to work with and can be the foundation for many successful applications.
Share this post: