Skip to main content

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

1

Start the Server

npx tsx app.ts
2

Test Authentication

Try accessing protected routes:
curl http://localhost:3000/api/v1/users
3

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"}'
4

Test CRUD Operations

curl -H "Authorization: Bearer valid-token" \
  http://localhost:3000/api/v1/users/1
5

Compare Both Approaches

Test both middleware styles:
curl -H "Authorization: Bearer valid-token" \
  http://localhost:3000/api/v1/users/1

Production Best Practices

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.
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 }
  });
}
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);
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

Build docs developers (and LLMs) love