Building REST APIs
Putting all routes in one file works for small projects, but quickly becomes unmanageable. Today you learn professional project structure, REST principles, routers, controllers, and validation.
What is REST?
REST (Representational State Transfer) is an architectural style for designing APIs. RESTful APIs use HTTP methods on resources (nouns) identified by URLs.
REST Principles
- Resources - Everything is a resource (users, products, orders)
- URLs identify resources - /users, /users/123, /users/123/posts
- HTTP methods define actions - GET reads, POST creates, etc.
- Stateless - Each request contains all info needed
- JSON responses - Standard format for data exchange
# Good RESTful URLs (use nouns, not verbs)
GET /api/users # Get all users
GET /api/users/123 # Get user 123
POST /api/users # Create new user
PUT /api/users/123 # Replace user 123
PATCH /api/users/123 # Update user 123
DELETE /api/users/123 # Delete user 123
# Nested resources
GET /api/users/123/posts # Posts by user 123
POST /api/users/123/posts # Create post for user 123
# BAD URLs (avoid verbs in paths)
GET /api/getUsers # Wrong - verb in URL
POST /api/createUser # Wrong - verb in URL
GET /api/deleteUser/123 # Wrong - using GET to delete
Professional Project Structure
Real projects separate concerns into different folders and files:
my-api/
├── node_modules/
├── src/
│ ├── routes/ # Route definitions
│ │ ├── index.js # Combines all routes
│ │ ├── userRoutes.js
│ │ └── productRoutes.js
│ ├── controllers/ # Request handlers
│ │ ├── userController.js
│ │ └── productController.js
│ ├── middleware/ # Custom middleware
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── models/ # Data models (Day 7+)
│ ├── utils/ # Helper functions
│ └── app.js # Express app setup
├── package.json
├── .env # Environment variables
└── server.js # Entry point
Express Router
Express Router creates modular route handlers. Each router handles routes for one resource:
const express = require('express');
const router = express.Router();
// Import controller functions
const userController = require('../controllers/userController');
// Define routes - paths are relative to where router is mounted
// If mounted at /api/users, these become /api/users and /api/users/:id
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
router.put('/:id', userController.updateUser);
router.delete('/:id', userController.deleteUser);
module.exports = router;
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
router.get('/', productController.getAllProducts);
router.get('/:id', productController.getProductById);
router.post('/', productController.createProduct);
router.put('/:id', productController.updateProduct);
router.delete('/:id', productController.deleteProduct);
module.exports = router;
Controllers
Controllers contain the logic for handling requests. They keep routes clean and logic testable:
// In-memory database (replace with real DB later)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
let nextId = 3;
// GET /api/users
exports.getAllUsers = (req, res) => {
// Support filtering via query params
let result = users;
if (req.query.name) {
result = result.filter(u =>
u.name.toLowerCase().includes(req.query.name.toLowerCase())
);
}
res.json({
count: result.length,
users: result
});
};
// GET /api/users/:id
exports.getUserById = (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
};
// POST /api/users
exports.createUser = (req, res) => {
const { name, email } = req.body;
// Validation
const errors = [];
if (!name) errors.push('Name is required');
if (!email) errors.push('Email is required');
if (email && !email.includes('@')) errors.push('Invalid email format');
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// Check for duplicate email
if (users.some(u => u.email === email)) {
return res.status(400).json({
error: 'Email already exists'
});
}
const newUser = {
id: nextId++,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
};
// PUT /api/users/:id
exports.updateUser = (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({
error: 'Name and email are required'
});
}
users[index] = { id, name, email };
res.json(users[index]);
};
// DELETE /api/users/:id
exports.deleteUser = (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).end();
};
Main Application File
const express = require('express');
const app = express();
// Import routes
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');
// Middleware
app.use(express.json());
// Request logging middleware
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
// Mount routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 404 handler - must be after all routes
app.use((req, res) => {
res.status(404).json({ error: 'Endpoint not found' });
});
// Error handler - must be last and have 4 parameters
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});
module.exports = app;
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`API available at http://localhost:${PORT}/api`);
});
Input Validation
Always validate user input. Never trust data from requests:
// src/utils/validators.js
exports.validateUser = (data) => {
const errors = [];
// Required fields
if (!data.name || data.name.trim() === '') {
errors.push('Name is required');
}
if (!data.email) {
errors.push('Email is required');
}
// Format validation
if (data.name && data.name.length < 2) {
errors.push('Name must be at least 2 characters');
}
if (data.name && data.name.length > 50) {
errors.push('Name must be less than 50 characters');
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (data.email && !emailRegex.test(data.email)) {
errors.push('Invalid email format');
}
if (data.age && (data.age < 0 || data.age > 150)) {
errors.push('Invalid age');
}
return {
isValid: errors.length === 0,
errors
};
};
// Usage in controller
const { validateUser } = require('../utils/validators');
exports.createUser = (req, res) => {
const validation = validateUser(req.body);
if (!validation.isValid) {
return res.status(400).json({ errors: validation.errors });
}
// Continue with creating user...
};
Error Handling Middleware
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// Error handling middleware (4 parameters!)
const errorHandler = (err, req, res, next) => {
// Log error for debugging
console.error('Error:', err);
// Handle known operational errors
if (err.isOperational) {
return res.status(err.statusCode).json({
error: err.message
});
}
// Handle JSON parsing errors
if (err instanceof SyntaxError && err.status === 400) {
return res.status(400).json({
error: 'Invalid JSON'
});
}
// Unknown errors - don't leak details in production
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
};
module.exports = { AppError, errorHandler };
// Usage in controller
const { AppError } = require('../middleware/errorHandler');
exports.getUserById = (req, res, next) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return next(new AppError('User not found', 404));
}
res.json(user);
};
Environment Variables
Never hardcode sensitive data. Use environment variables:
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
# Install dotenv
npm install dotenv
// At the top of server.js
require('dotenv').config();
// Now access variables
const PORT = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
NEVER commit .env files to git! Add .env to your .gitignore file. Instead, provide a .env.example with placeholder values.
Video Resources
Knowledge Check: Day 5
1. What is the purpose of Express Router?
2. Which URL pattern is RESTful?
3. What should you do with sensitive data like API keys?
Build a Complete Blog API
Create a fully structured REST API with:
- Proper project structure (routes, controllers, middleware folders)
- Posts resource: GET all, GET one, POST, PUT, DELETE
- Comments resource nested under posts: /api/posts/:postId/comments
- Input validation for all POST/PUT requests
- Custom error handling middleware
- Request logging middleware
- Environment variables for PORT
Day 5 Complete!
You now know how to build professional REST APIs with proper structure. You understand routers, controllers, validation, error handling, and environment variables. This is how production APIs are built.
Tomorrow, we add persistence! No more losing data when the server restarts. We will learn MongoDB - a powerful NoSQL database.