Complete REST API Example
This example demonstrates a production-ready API with both middleware approaches, async error handling, custom error types, and best practices.import express, { Request, Response, NextFunction } from 'express';
import {
StatusFlow,
StatusFlowCodes,
statusFlowMiddleware,
BadRequestException,
UnauthorizedException,
NotFoundException,
ConflictException,
InternalServerErrorException,
httpErrorMiddleware
} from 'status-flow';
const app = express();
app.use(express.json());
// Mock database
interface User {
id: string;
email: string;
name: string;
role: string;
}
const users: User[] = [
{ id: '1', email: 'admin@example.com', name: 'Admin User', role: 'admin' },
{ id: '2', email: 'user@example.com', name: 'Regular User', role: 'user' }
];
// Custom error type for validation
class ValidationError extends Error {
constructor(
message: string,
public fields: Record<string, string>
) {
super(message);
this.name = 'ValidationError';
}
}
// Async wrapper to catch errors
const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Authentication middleware
const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
throw new UnauthorizedException('Authentication token required');
}
if (token !== 'valid-token') {
throw new UnauthorizedException('Invalid authentication token');
}
// Attach user to request
(req as any).user = { id: '1', role: 'admin' };
next();
};
// Authorization middleware
const authorize = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user;
if (!user) {
throw new UnauthorizedException('User not authenticated');
}
if (!roles.includes(user.role)) {
throw new ForbiddenException('Insufficient permissions');
}
next();
};
};
// Validation helper
const validateUser = (data: any) => {
const errors: Record<string, string> = {};
if (!data.email || !data.email.includes('@')) {
errors.email = 'Valid email is required';
}
if (!data.name || data.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
}
if (Object.keys(errors).length > 0) {
throw new ValidationError('Validation failed', errors);
}
};
// Routes using StatusFlow approach
const statusFlowRoutes = express.Router();
// Health check
statusFlowRoutes.get('/health', (req, res) => {
res.json(
StatusFlow({
code: StatusFlowCodes.OK,
extra: {
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString()
}
})
);
});
// Get all users
statusFlowRoutes.get('/users', authenticate, asyncHandler(async (req, res) => {
// Simulate async database call
await new Promise(resolve => setTimeout(resolve, 100));
res.json(
StatusFlow({
code: StatusFlowCodes.OK,
extra: {
users,
total: users.length
}
})
);
}));
// Get user by ID
statusFlowRoutes.get('/users/:id', authenticate, asyncHandler(async (req, res, next) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return next({
code: StatusFlowCodes.NOT_FOUND,
message: 'User not found',
extra: { userId: req.params.id }
});
}
res.json(
StatusFlow({
code: StatusFlowCodes.OK,
extra: { user }
})
);
}));
// Create user
statusFlowRoutes.post('/users', authenticate, authorize('admin'), asyncHandler(async (req, res, next) => {
try {
validateUser(req.body);
} catch (error) {
if (error instanceof ValidationError) {
return next({
code: StatusFlowCodes.BAD_REQUEST,
message: error.message,
extra: { errors: error.fields }
});
}
throw error;
}
// Check if user exists
if (users.find(u => u.email === req.body.email)) {
return next({
code: StatusFlowCodes.CONFLICT,
message: 'User with this email already exists',
extra: { email: req.body.email }
});
}
const newUser: User = {
id: String(users.length + 1),
email: req.body.email,
name: req.body.name,
role: req.body.role || 'user'
};
users.push(newUser);
res.status(201).json(
StatusFlow({
code: StatusFlowCodes.CREATED,
extra: { user: newUser }
})
);
}));
// Routes using HTTP Exceptions approach
const exceptionRoutes = express.Router();
// Get all posts
exceptionRoutes.get('/posts', authenticate, asyncHandler(async (req, res) => {
const posts = [
{ id: '1', title: 'First Post', author: 'John' },
{ id: '2', title: 'Second Post', author: 'Jane' }
];
res.json({
status: 200,
message: 'Posts retrieved successfully',
data: { posts, total: posts.length }
});
}));
// Get post by ID
exceptionRoutes.get('/posts/:id', authenticate, asyncHandler(async (req, res) => {
// Simulate database lookup
await new Promise(resolve => setTimeout(resolve, 50));
if (req.params.id === '999') {
throw new NotFoundException('Post not found');
}
res.json({
status: 200,
message: 'Post found',
data: {
id: req.params.id,
title: 'Sample Post',
content: 'This is a sample post'
}
});
}));
// Create post
exceptionRoutes.post('/posts', authenticate, asyncHandler(async (req, res) => {
if (!req.body.title) {
throw new BadRequestException('Title is required', { field: 'title' });
}
// Simulate async operation that might fail
const shouldFail = Math.random() > 0.8;
if (shouldFail) {
throw new InternalServerErrorException('Failed to save post to database');
}
res.status(201).json({
status: 201,
message: 'Post created successfully',
data: {
id: '3',
title: req.body.title,
content: req.body.content || ''
}
});
}));
// Mount routers
app.use('/api/v1', statusFlowRoutes);
app.use('/api/v2', exceptionRoutes);
// 404 handler for undefined routes
app.use((req, res, next) => {
next({
code: StatusFlowCodes.NOT_FOUND,
message: 'Route not found',
extra: {
path: req.path,
method: req.method
}
});
});
// Error handling - StatusFlow middleware for /api/v1
app.use('/api/v1', statusFlowMiddleware);
// Error handling - HTTP Exception middleware for /api/v2
app.use('/api/v2', httpErrorMiddleware);
// Global error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error('Unhandled error:', err);
res.status(500).json({
status: 500,
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Advanced API server running on http://localhost:${PORT}`);
console.log(`- StatusFlow routes: http://localhost:${PORT}/api/v1`);
console.log(`- Exception routes: http://localhost:${PORT}/api/v2`);
});
export default app;
Testing the Advanced API
Test Validation
Test input validation:
Create User (Validation Error)
curl -X POST http://localhost:3000/api/v1/users \
-H "Authorization: Bearer valid-token" \
-H "Content-Type: application/json" \
-d '{"email": "invalid", "name": "A"}'
Test CRUD Operations
curl -H "Authorization: Bearer valid-token" \
http://localhost:3000/api/v1/users/1
Production Best Practices
Async Error Handling
Async Error Handling
Always wrap async route handlers to catch errors:
const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
app.get('/async', asyncHandler(async (req, res) => {
const data = await someAsyncOperation();
res.json(data);
}));
Without
asyncHandler, unhandled promise rejections won’t be caught by your error middleware.Custom Validation Errors
Custom Validation Errors
Create custom error types for better error handling:
class ValidationError extends Error {
constructor(
message: string,
public fields: Record<string, string>
) {
super(message);
this.name = 'ValidationError';
}
}
// Use in middleware
if (error instanceof ValidationError) {
return next({
code: StatusFlowCodes.BAD_REQUEST,
message: error.message,
extra: { errors: error.fields }
});
}
Middleware Organization
Middleware Organization
Structure your middleware in the correct order:
// 1. Body parsers
app.use(express.json());
// 2. Global middleware (logging, CORS, etc.)
app.use(cors());
app.use(logger);
// 3. Routes
app.use('/api/v1', statusFlowRoutes);
app.use('/api/v2', exceptionRoutes);
// 4. 404 handler
app.use((req, res, next) => {
next({ code: 404, message: 'Not found' });
});
// 5. Error handlers (MUST be last)
app.use('/api/v1', statusFlowMiddleware);
app.use('/api/v2', httpErrorMiddleware);
Environment-Specific Error Details
Environment-Specific Error Details
Show detailed errors only in development:
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
const isDev = process.env.NODE_ENV === 'development';
res.status(err.status || 500).json({
status: err.status || 500,
message: err.message,
// Only include stack trace in development
stack: isDev ? err.stack : undefined,
// Only include detailed errors in development
details: isDev ? err.details : undefined
});
});
Combining Both Approaches
You can use both
StatusFlow and HTTP exceptions in the same application. Use StatusFlow for standardized bilingual responses, and exceptions for simple, typed error handling.// StatusFlow for client-facing API with rich metadata
app.use('/api/public', publicRoutes);
app.use('/api/public', statusFlowMiddleware);
// Exceptions for internal API with simple responses
app.use('/api/internal', internalRoutes);
app.use('/api/internal', httpErrorMiddleware);
Key Patterns
Async Handlers
Always wrap async routes to properly catch errors and pass them to error middleware.
Authentication
Use middleware to authenticate and authorize before accessing protected routes.
Validation
Validate input early and return detailed error messages with field-level information.
Router Separation
Use Express routers to organize routes and apply different error handling strategies.
Next Steps
Bilingual API
Learn how to create APIs with multi-language support
API Reference
Explore the complete API documentation