01How The Internet Works 02Your First Node.js Server 03HTTP Methods and JSON 04Express.js Framework 05Building REST APIs 06MongoDB Fundamentals 07Mongoose ODM 08Data Modeling 09Authentication Basics 10JWT Implementation

Settings

Theme
Sand
Cloud
Midnight
Forest
Sunset
Purple
Ocean
Crimson
Font
Merriweather
Inter
JetBrains
Space Grotesk
Fira Code
Playfair
Font Size
100%
Bookmark
No bookmark set
Day 5

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
RESTful URL Patterns
# 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:

Project Structure
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:

src/routes/userRoutes.js
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;
src/routes/productRoutes.js
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:

src/controllers/userController.js
// 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

src/app.js
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;
server.js
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:

Basic Validation Function
// 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

src/middleware/errorHandler.js
// 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:

.env file
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
Using dotenv
# 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;
Security

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?

ATo handle database connections
BTo create modular, mountable route handlers
CTo validate request data
DTo serve static files

2. Which URL pattern is RESTful?

ADELETE /api/users/123
BGET /api/deleteUser/123
CPOST /api/removeUser
DGET /api/users/delete/123

3. What should you do with sensitive data like API keys?

AHardcode them in your source files
BCommit them to git for backup
CStore them in environment variables
DPut them in comments for documentation
Project

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.