Skip to main content
โšก Calmops

Building REST APIs with Node.js

Building REST APIs with Node.js

REST APIs are the foundation of modern web applications. This article covers REST API development with Node.js.

Introduction

REST APIs provide:

  • Standardized endpoints
  • HTTP methods
  • Status codes
  • Request/response formats
  • Scalability

Understanding REST APIs helps you:

  • Build web services
  • Create scalable backends
  • Integrate with frontends
  • Design clean APIs
  • Handle data exchange

REST API Fundamentals

HTTP Methods and Status Codes

const express = require('express');
const app = express();

app.use(express.json());

// โœ… Good: GET - Retrieve resources
app.get('/api/users', (req, res) => {
  res.status(200).json([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ]);
});

// โœ… Good: GET - Retrieve single resource
app.get('/api/users/:id', (req, res) => {
  const user = { id: req.params.id, name: 'John' };
  res.status(200).json(user);
});

// โœ… Good: POST - Create resource
app.post('/api/users', (req, res) => {
  const newUser = { id: 3, ...req.body };
  res.status(201).json(newUser);
});

// โœ… Good: PUT - Update resource
app.put('/api/users/:id', (req, res) => {
  const updatedUser = { id: req.params.id, ...req.body };
  res.status(200).json(updatedUser);
});

// โœ… Good: PATCH - Partial update
app.patch('/api/users/:id', (req, res) => {
  const user = { id: req.params.id, name: 'John', ...req.body };
  res.status(200).json(user);
});

// โœ… Good: DELETE - Remove resource
app.delete('/api/users/:id', (req, res) => {
  res.status(204).send();
});

// โœ… Good: Status codes
// 200 OK - Success
// 201 Created - Resource created
// 204 No Content - Success, no content
// 400 Bad Request - Invalid input
// 401 Unauthorized - Authentication required
// 403 Forbidden - Access denied
// 404 Not Found - Resource not found
// 500 Internal Server Error - Server error

API Response Format

// โœ… Good: Consistent response format
const sendResponse = (res, status, data, message) => {
  res.status(status).json({
    success: status < 400,
    status,
    message,
    data
  });
};

app.get('/api/users', (req, res) => {
  const users = [{ id: 1, name: 'John' }];
  sendResponse(res, 200, users, 'Users retrieved successfully');
});

app.post('/api/users', (req, res) => {
  const newUser = { id: 1, ...req.body };
  sendResponse(res, 201, newUser, 'User created successfully');
});

// โœ… Good: Error response format
app.get('/api/users/:id', (req, res) => {
  const user = null;
  if (!user) {
    return sendResponse(res, 404, null, 'User not found');
  }
  sendResponse(res, 200, user, 'User retrieved successfully');
});

// โœ… Good: Pagination response
app.get('/api/users', (req, res) => {
  const page = req.query.page || 1;
  const limit = req.query.limit || 10;
  const users = [{ id: 1, name: 'John' }];

  res.json({
    success: true,
    data: users,
    pagination: {
      page,
      limit,
      total: 100,
      pages: Math.ceil(100 / limit)
    }
  });
});

API Design Patterns

RESTful Endpoints

const express = require('express');
const app = express();

app.use(express.json());

// โœ… Good: Resource-based endpoints
// Users
app.get('/api/users', (req, res) => { /* List users */ });
app.post('/api/users', (req, res) => { /* Create user */ });
app.get('/api/users/:id', (req, res) => { /* Get user */ });
app.put('/api/users/:id', (req, res) => { /* Update user */ });
app.delete('/api/users/:id', (req, res) => { /* Delete user */ });

// Posts
app.get('/api/posts', (req, res) => { /* List posts */ });
app.post('/api/posts', (req, res) => { /* Create post */ });
app.get('/api/posts/:id', (req, res) => { /* Get post */ });
app.put('/api/posts/:id', (req, res) => { /* Update post */ });
app.delete('/api/posts/:id', (req, res) => { /* Delete post */ });

// โœ… Good: Nested resources
app.get('/api/users/:userId/posts', (req, res) => {
  const userId = req.params.userId;
  res.json([{ id: 1, userId, title: 'Post 1' }]);
});

app.post('/api/users/:userId/posts', (req, res) => {
  const userId = req.params.userId;
  res.status(201).json({ id: 1, userId, ...req.body });
});

// โœ… Good: Query parameters for filtering
app.get('/api/users', (req, res) => {
  const { role, status, page = 1, limit = 10 } = req.query;
  // Filter users based on query parameters
  res.json([]);
});

// โœ… Good: Sorting and pagination
app.get('/api/users', (req, res) => {
  const { sort = 'createdAt', order = 'desc', page = 1, limit = 10 } = req.query;
  const offset = (page - 1) * limit;
  // Apply sorting and pagination
  res.json([]);
});

API Versioning

const express = require('express');
const app = express();

// โœ… Good: URL versioning
app.get('/api/v1/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

app.get('/api/v2/users', (req, res) => {
  res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
});

// โœ… Good: Header versioning
app.get('/api/users', (req, res) => {
  const version = req.headers['api-version'] || '1';
  if (version === '2') {
    res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
  } else {
    res.json([{ id: 1, name: 'John' }]);
  }
});

// โœ… Good: Router versioning
const v1Router = express.Router();
const v2Router = express.Router();

v1Router.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

v2Router.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
});

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

Complete API Example

User API Implementation

// controllers/userController.js
const User = require('../models/User');

const getUsers = async (req, res, next) => {
  try {
    const { page = 1, limit = 10, role } = req.query;
    const offset = (page - 1) * limit;

    let query = {};
    if (role) query.role = role;

    const users = await User.find(query)
      .limit(limit)
      .skip(offset);

    const total = await User.countDocuments(query);

    res.json({
      success: true,
      data: users,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (err) {
    next(err);
  }
};

const getUserById = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found'
      });
    }
    res.json({ success: true, data: user });
  } catch (err) {
    next(err);
  }
};

const createUser = async (req, res, next) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json({ success: true, data: user });
  } catch (err) {
    next(err);
  }
};

const updateUser = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found'
      });
    }
    res.json({ success: true, data: user });
  } catch (err) {
    next(err);
  }
};

const deleteUser = async (req, res, next) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found'
      });
    }
    res.status(204).send();
  } catch (err) {
    next(err);
  }
};

module.exports = {
  getUsers,
  getUserById,
  createUser,
  updateUser,
  deleteUser
};

// routes/users.js
const express = require('express');
const { body } = require('express-validator');
const validate = require('../middleware/validate');
const authenticate = require('../middleware/auth');
const userController = require('../controllers/userController');

const router = express.Router();

router.get('/', userController.getUsers);

router.post(
  '/',
  [
    body('name').notEmpty(),
    body('email').isEmail(),
    body('password').isLength({ min: 8 })
  ],
  validate,
  userController.createUser
);

router.get('/:id', userController.getUserById);

router.put(
  '/:id',
  authenticate,
  [
    body('name').optional().notEmpty(),
    body('email').optional().isEmail()
  ],
  validate,
  userController.updateUser
);

router.delete('/:id', authenticate, userController.deleteUser);

module.exports = router;

// app.js
const express = require('express');
const userRoutes = require('./routes/users');
const errorHandler = require('./middleware/errorHandler');

const app = express();

app.use(express.json());
app.use('/api/users', userRoutes);
app.use(errorHandler);

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

API Documentation

OpenAPI/Swagger Documentation

// โœ… Good: Swagger setup
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'User API',
      version: '1.0.0'
    },
    servers: [
      { url: 'http://localhost:3000' }
    ]
  },
  apis: ['./routes/*.js']
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

// โœ… Good: Document endpoints
/**
 * @swagger
 * /api/users:
 *   get:
 *     summary: Get all users
 *     responses:
 *       200:
 *         description: List of users
 */
app.get('/api/users', (req, res) => {
  res.json([]);
});

Best Practices

  1. Use consistent naming:

    // โœ… Good: Consistent naming
    app.get('/api/users', ...);
    app.get('/api/posts', ...);
    
    // โŒ Bad: Inconsistent naming
    app.get('/api/users', ...);
    app.get('/api/get-posts', ...);
    
  2. Use proper status codes:

    // โœ… Good: Proper status codes
    res.status(201).json(newUser);  // Created
    res.status(204).send();          // No content
    res.status(400).json(error);     // Bad request
    
    // โŒ Bad: Always 200
    res.json(newUser);
    
  3. Validate input:

    // โœ… Good: Validate input
    app.post('/api/users', validate, (req, res) => {
      // Process validated data
    });
    
    // โŒ Bad: No validation
    app.post('/api/users', (req, res) => {
      // Process unvalidated data
    });
    

Summary

REST APIs are essential. Key takeaways:

  • Use HTTP methods correctly
  • Design resource-based endpoints
  • Use proper status codes
  • Implement pagination and filtering
  • Version your APIs
  • Validate input
  • Document endpoints
  • Handle errors properly

Next Steps

Comments