Module 16: JavaScript Backend

Building RESTful APIs with Express.js

Understanding REST Architecture

REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on a stateless, client-server communication protocol, typically HTTP, and treats server objects as resources that can be created, read, updated, or deleted.

Analogy: Think of a RESTful API as a well-organized library. Resources (books) are organized by category (endpoints), each with a unique identifier (URI). You can browse the catalog (GET), add new books (POST), update book information (PUT/PATCH), or remove books (DELETE). The library maintains a consistent system for all these operations, regardless of the type of book, making it intuitive for anyone to use.

graph TD A[HTTP Method + URL Path] --> B{RESTful API} B --> C[Resource 1] B --> D[Resource 2] B --> E[Resource 3] C --> F[Identify Resource] F --> G[Process Request] G --> H[Return Representation] style B fill:#e3f2fd,stroke:#1976d2 style C fill:#e8f5e9,stroke:#388e3c style D fill:#e8f5e9,stroke:#388e3c style E fill:#e8f5e9,stroke:#388e3c

Key Principles of REST

Real-world example: Major platforms like Twitter, GitHub, and Stripe use RESTful APIs to expose their services. For instance, GitHub's API lets you access repositories, issues, and pull requests as resources with URIs like /repos/:owner/:repo or /repos/:owner/:repo/issues.

HTTP Methods in REST

RESTful APIs use HTTP methods to define the operation being performed on resources:

Method Operation Description Example
GET Read Retrieve resources without modifying them GET /users
POST Create Create new resources POST /users
PUT Update Replace an entire resource PUT /users/123
PATCH Update Partially update a resource PATCH /users/123
DELETE Delete Remove resources DELETE /users/123

HTTP Method Semantics


// GET - Retrieve all users
app.get('/api/users', (req, res) => {
  // Implementation to fetch users
  res.json(users);
});

// GET - Retrieve a specific user
app.get('/api/users/:id', (req, res) => {
  // Implementation to fetch a specific user
  const user = findUserById(req.params.id);
  res.json(user);
});

// POST - Create a new user
app.post('/api/users', (req, res) => {
  // Implementation to create a user
  const newUser = createUser(req.body);
  res.status(201).json(newUser);
});

// PUT - Replace a user completely
app.put('/api/users/:id', (req, res) => {
  // Implementation to replace a user
  const updatedUser = replaceUser(req.params.id, req.body);
  res.json(updatedUser);
});

// PATCH - Update a user partially
app.patch('/api/users/:id', (req, res) => {
  // Implementation to update specific fields
  const updatedUser = updateUser(req.params.id, req.body);
  res.json(updatedUser);
});

// DELETE - Remove a user
app.delete('/api/users/:id', (req, res) => {
  // Implementation to delete a user
  deleteUser(req.params.id);
  res.status(204).send();
});
            

PUT vs PATCH

Understanding the difference between PUT and PATCH is important:


// Example: Original user
// { "id": 123, "name": "John", "email": "john@example.com", "role": "user" }

// PUT request - Replaces the entire resource
// Request body: { "name": "John Updated", "email": "john.updated@example.com" }
// Result: { "id": 123, "name": "John Updated", "email": "john.updated@example.com", "role": null }

// PATCH request - Updates only specified fields
// Request body: { "name": "John Updated", "email": "john.updated@example.com" }
// Result: { "id": 123, "name": "John Updated", "email": "john.updated@example.com", "role": "user" }
            
graph TD subgraph "PUT - Full Replacement" A1[Original Resource] --> B1[PUT Request] B1 --> C1[Replace Entire Resource] C1 --> D1[New Resource State] style A1 fill:#bbdefb style B1 fill:#c8e6c9 style C1 fill:#ffccbc style D1 fill:#d1c4e9 end subgraph "PATCH - Partial Update" A2[Original Resource] --> B2[PATCH Request] B2 --> C2[Modify Specified Fields] A2 --> D2[Merge Changes] C2 --> D2 D2 --> E2[Updated Resource State] style A2 fill:#bbdefb style B2 fill:#c8e6c9 style C2 fill:#ffccbc style D2 fill:#ffe0b2 style E2 fill:#d1c4e9 end

Resource URI Design

URI (Uniform Resource Identifier) design is crucial for a clean, intuitive API. Follow these principles:

Resource Naming


// Good URI design
app.get('/api/users', getAllUsers);
app.get('/api/users/:id', getUserById);
app.get('/api/blog-posts', getAllBlogPosts);
app.get('/api/blog-posts/:id/comments', getPostComments);

// Poor URI design - avoid these
app.get('/api/getUsers', getUsers); // Uses verb
app.get('/api/user/:id', getUserById); // Uses singular
app.get('/api/userManagement', manageUsers); // Abstract concept
app.get('/api/blogPosts', getBlogPosts); // Uses camelCase
            

Hierarchical Relationships

Express nested relationships using hierarchical URIs:


// Parent-child relationships
app.get('/api/users/:userId/orders', getUserOrders);
app.get('/api/users/:userId/orders/:orderId', getUserOrder);
app.post('/api/users/:userId/orders', createUserOrder);

// Alternative: Using query parameters for filtering
app.get('/api/orders?userId=123', getOrdersByUser);
            

Query Parameters vs. Path Parameters

Use path parameters for identifying resources and query parameters for filtering, sorting, pagination, etc.:


// Path parameters for resource identification
app.get('/api/users/:id', getUserById);

// Query parameters for filtering, sorting, pagination
app.get('/api/users', (req, res) => {
  const { role, status, sort, page, limit } = req.query;
  
  // Filter users by role and status
  let filteredUsers = users;
  if (role) {
    filteredUsers = filteredUsers.filter(user => user.role === role);
  }
  if (status) {
    filteredUsers = filteredUsers.filter(user => user.status === status);
  }
  
  // Sort users
  if (sort) {
    const [field, order] = sort.split(':');
    filteredUsers.sort((a, b) => {
      return order === 'desc' 
        ? b[field].localeCompare(a[field]) 
        : a[field].localeCompare(b[field]);
    });
  }
  
  // Paginate results
  const pageNum = parseInt(page) || 1;
  const limitNum = parseInt(limit) || 10;
  const startIndex = (pageNum - 1) * limitNum;
  const paginatedUsers = filteredUsers.slice(startIndex, startIndex + limitNum);
  
  res.json({
    data: paginatedUsers,
    meta: {
      total: filteredUsers.length,
      page: pageNum,
      limit: limitNum,
      pages: Math.ceil(filteredUsers.length / limitNum)
    }
  });
});
            

Analogy: Think of URI design like a postal address system. Path parameters are like the building number and street name, uniquely identifying a location. Query parameters are like additional instructions for the mail carrier (e.g., "Leave package at back door" or "Signature required"). They modify how the delivery happens but don't change the address itself.

HTTP Status Codes

Proper HTTP status codes communicate the result of requests clearly:

Common Status Codes in REST APIs

Code Message Usage
200 OK Successful request
201 Created Resource created successfully
204 No Content Successful request with no response body (e.g., DELETE)
400 Bad Request Invalid request format or parameters
401 Unauthorized Authentication required
403 Forbidden Authenticated but not authorized
404 Not Found Resource not found
409 Conflict Request conflicts with current state (e.g., duplicate entry)
422 Unprocessable Entity Validation errors
500 Internal Server Error Server-side error

Using Status Codes in Express


// Successful responses
app.get('/api/users', (req, res) => {
  // Success with data
  res.status(200).json(users);
});

app.post('/api/users', (req, res) => {
  // Resource created
  const newUser = createUser(req.body);
  res.status(201).json(newUser);
});

app.delete('/api/users/:id', (req, res) => {
  // Success, no content to return
  deleteUser(req.params.id);
  res.status(204).send();
});

// Client error responses
app.post('/api/users', (req, res) => {
  // Validation error
  if (!req.body.email) {
    return res.status(400).json({ error: 'Email is required' });
  }
  
  // Email already exists
  if (userExists(req.body.email)) {
    return res.status(409).json({ error: 'Email already in use' });
  }
  
  // Create user...
});

app.get('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
});

app.put('/api/admin/settings', (req, res) => {
  if (!req.user.isAdmin) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  
  // Update settings...
});

// Server error response
app.get('/api/reports', (req, res) => {
  try {
    const reports = generateReports();
    res.json(reports);
  } catch (error) {
    console.error('Report generation error:', error);
    res.status(500).json({ error: 'Failed to generate reports' });
  }
});
            

Real-world example: The Stripe API uses HTTP status codes extensively to communicate different error conditions. They return 400 errors for invalid parameters, 401 for authentication failures, 402 for payment failures, and 429 for rate limiting. Their detailed error responses make it easy for developers to debug issues and provide clear messages to end users.

Request and Response Formatting

Consistent request and response formats improve API usability and developer experience.

Content Negotiation

Use content negotiation to support multiple formats:


app.get('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  // Check Accept header to determine response format
  const acceptHeader = req.get('Accept');
  
  if (acceptHeader === 'application/xml') {
    // Convert user to XML and send
    res.set('Content-Type', 'application/xml');
    res.send(convertToXML(user));
  } else {
    // Default to JSON
    res.json(user);
  }
});
            

Standard Response Structure

Use a consistent structure for all responses:


// Success response
{
  "data": [...],
  "meta": {
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 100,
      "pages": 10
    }
  }
}

// Error response
{
  "error": {
    "message": "Resource not found",
    "code": "NOT_FOUND",
    "details": [...]
  }
}
            

Response Formatting Middleware


// middleware/response-formatter.js
const formatResponse = (req, res, next) => {
  // Store the original json method
  const originalJson = res.json;
  
  // Override the json method
  res.json = function(body) {
    // Skip if already formatted
    if (body && (body.data || body.error)) {
      return originalJson.call(this, body);
    }
    
    // Format the response
    const formattedBody = {
      data: body,
      meta: {
        timestamp: new Date().toISOString(),
        // Include other metadata...
      }
    };
    
    // Call the original json method with the formatted body
    return originalJson.call(this, formattedBody);
  };
  
  next();
};

// Use the middleware
app.use(formatResponse);
            

Error Response Formatting


// middleware/error-handler.js
const errorHandler = (err, req, res, next) => {
  // Set default status code
  const statusCode = err.statusCode || 500;
  const errorCode = err.errorCode || 'INTERNAL_ERROR';
  
  // Log the error
  console.error(`Error ${statusCode}: ${err.message}`);
  if (statusCode === 500) {
    console.error(err.stack);
  }
  
  // Format the error response
  const errorResponse = {
    error: {
      message: err.message || 'An unexpected error occurred',
      code: errorCode
    }
  };
  
  // Add validation errors if available
  if (err.errors) {
    errorResponse.error.details = err.errors;
  }
  
  // Include stack trace in development
  if (process.env.NODE_ENV === 'development' && err.stack) {
    errorResponse.error.stack = err.stack;
  }
  
  res.status(statusCode).json(errorResponse);
};

// Add the error handler as the last middleware
app.use(errorHandler);
            

Analogy: Standardized response formatting is like a well-designed report template in a corporation. No matter which department submits a report, it follows the same structure—making it immediately familiar to anyone who reads it. The reader always knows where to find the summary, detailed data, and conclusions, regardless of the report's content.

HATEOAS and API Discoverability

HATEOAS (Hypermedia as the Engine of Application State) makes APIs self-documenting by including relevant links with responses.

Adding Resource Links


app.get('/api/users/:id', (req, res) => {
  const user = findUserById(req.params.id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  // Add relevant links
  const links = {
    self: `${req.protocol}://${req.get('host')}/api/users/${user.id}`,
    orders: `${req.protocol}://${req.get('host')}/api/users/${user.id}/orders`,
    update: `${req.protocol}://${req.get('host')}/api/users/${user.id}`,
    delete: `${req.protocol}://${req.get('host')}/api/users/${user.id}`
  };
  
  res.json({
    data: user,
    links
  });
});

app.get('/api/users', (req, res) => {
  // Pagination logic...
  
  // Add pagination links
  const links = {
    self: `${req.protocol}://${req.get('host')}/api/users?page=${page}&limit=${limit}`,
    first: `${req.protocol}://${req.get('host')}/api/users?page=1&limit=${limit}`,
    last: `${req.protocol}://${req.get('host')}/api/users?page=${totalPages}&limit=${limit}`
  };
  
  if (page > 1) {
    links.prev = `${req.protocol}://${req.get('host')}/api/users?page=${page - 1}&limit=${limit}`;
  }
  
  if (page < totalPages) {
    links.next = `${req.protocol}://${req.get('host')}/api/users?page=${page + 1}&limit=${limit}`;
  }
  
  res.json({
    data: users,
    meta: {
      pagination: { page, limit, total, totalPages }
    },
    links
  });
});
            

Link Header Approach

An alternative approach is using the Link header:


app.get('/api/users', (req, res) => {
  // Pagination logic...
  
  // Set Link headers for pagination
  const linkHeader = [];
  
  linkHeader.push(`<${req.protocol}://${req.get('host')}/api/users?page=${page}&limit=${limit}>; rel="self"`);
  linkHeader.push(`<${req.protocol}://${req.get('host')}/api/users?page=1&limit=${limit}>; rel="first"`);
  linkHeader.push(`<${req.protocol}://${req.get('host')}/api/users?page=${totalPages}&limit=${limit}>; rel="last"`);
  
  if (page > 1) {
    linkHeader.push(`<${req.protocol}://${req.get('host')}/api/users?page=${page - 1}&limit=${limit}>; rel="prev"`);
  }
  
  if (page < totalPages) {
    linkHeader.push(`<${req.protocol}://${req.get('host')}/api/users?page=${page + 1}&limit=${limit}>; rel="next"`);
  }
  
  res.set('Link', linkHeader.join(', '));
  res.json({ data: users });
});
            
graph LR A[Client] -->|Request| B[RESTful API] B -->|Response with Links| A subgraph "Response with HATEOAS" C[Resource Data] D[Links To Related Resources] E[Links To Available Actions] F[Pagination Links] end style C fill:#bbdefb style D fill:#c8e6c9 style E fill:#ffccbc style F fill:#d1c4e9

Real-world example: GitHub's API implements HATEOAS by including navigation links with responses. When you fetch a list of repositories, the response includes URLs for the next, previous, first, and last pages, allowing client applications to navigate the results without having to construct URLs manually. This makes their API more discoverable and self-documenting.

Versioning RESTful APIs

API versioning helps evolve your API without breaking existing clients. There are several approaches:

URI Path Versioning


// Version in the URI path
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// v1Router.js
const express = require('express');
const router = express.Router();

router.get('/users', (req, res) => {
  // v1 implementation
  res.json(users);
});

module.exports = router;

// v2Router.js
const express = require('express');
const router = express.Router();

router.get('/users', (req, res) => {
  // v2 implementation with enhanced data
  res.json({
    data: users,
    meta: { count: users.length }
  });
});

module.exports = router;
            

Query Parameter Versioning


app.get('/api/users', (req, res) => {
  const version = req.query.version || '1';
  
  if (version === '1') {
    // v1 implementation
    return res.json(users);
  } else if (version === '2') {
    // v2 implementation
    return res.json({
      data: users,
      meta: { count: users.length }
    });
  } else {
    return res.status(400).json({ error: 'Unsupported API version' });
  }
});
            

Header Versioning


app.get('/api/users', (req, res) => {
  const version = req.get('Accept-Version') || '1';
  
  if (version === '1') {
    // v1 implementation
    return res.json(users);
  } else if (version === '2') {
    // v2 implementation
    return res.json({
      data: users,
      meta: { count: users.length }
    });
  } else {
    return res.status(400).json({ error: 'Unsupported API version' });
  }
});
            

Content Type Versioning


app.get('/api/users', (req, res) => {
  const acceptHeader = req.get('Accept');
  
  if (acceptHeader === 'application/vnd.myapi.v1+json') {
    // v1 implementation
    return res.json(users);
  } else if (acceptHeader === 'application/vnd.myapi.v2+json') {
    // v2 implementation
    return res.json({
      data: users,
      meta: { count: users.length }
    });
  } else {
    // Default to latest version
    return res.json({
      data: users,
      meta: { count: users.length }
    });
  }
});
            

Analogy: API versioning is like product model numbers. When a manufacturer releases a new version of a product with significant changes, they increment the model number rather than changing the original. This allows customers with the older model to continue using it while clearly indicating which version they have. Similarly, API versioning lets clients continue using the API version they're compatible with while allowing new clients to use the latest features.

Express Implementation of a RESTful API

Let's put everything together in a complete Express implementation of a RESTful API:

Project Structure


project/
├── app.js                  # Main application file
├── routes/
│   ├── index.js            # Route aggregator
│   ├── user.routes.js      # User routes
│   └── product.routes.js   # Product routes
├── controllers/
│   ├── user.controller.js  # User controller
│   └── product.controller.js # Product controller
├── models/
│   ├── user.model.js       # User model
│   └── product.model.js    # Product model
├── middleware/
│   ├── auth.js             # Authentication middleware
│   ├── error-handler.js    # Error handling middleware
│   └── response-formatter.js # Response formatting middleware
└── config/
    └── index.js            # Configuration settings
            

Entry Point (app.js)


// app.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const routes = require('./routes');
const errorHandler = require('./middleware/error-handler');
const responseFormatter = require('./middleware/response-formatter');
const config = require('./config');

// Create Express app
const app = express();

// Connect to database
mongoose.connect(config.dbUri)
  .then(() => console.log('Connected to database'))
  .catch(err => console.error('Database connection error:', err));

// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Add response formatter
app.use(responseFormatter);

// Mount routes
app.use('/api', routes);

// Handle 404 errors
app.use((req, res) => {
  res.status(404).json({
    error: {
      message: 'Resource not found',
      code: 'NOT_FOUND'
    }
  });
});

// Error handling middleware
app.use(errorHandler);

// Start server
const PORT = config.port || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;
            

Routes (routes/user.routes.js)


// routes/user.routes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');
const { authenticate, authorize } = require('../middleware/auth');

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

// Protected routes
router.post('/', authenticate, userController.createUser);
router.put('/:id', authenticate, authorize('admin'), userController.updateUser);
router.patch('/:id', authenticate, authorize('admin'), userController.updateUserPartially);
router.delete('/:id', authenticate, authorize('admin'), userController.deleteUser);

// Nested routes
router.get('/:id/orders', authenticate, userController.getUserOrders);

module.exports = router;
            

Controller (controllers/user.controller.js)


// controllers/user.controller.js
const User = require('../models/user.model');
const Order = require('../models/order.model');
const AppError = require('../utils/app-error');

// Helper function to build resource links
const buildUserLinks = (req, userId) => {
  const baseUrl = `${req.protocol}://${req.get('host')}/api/users`;
  
  return {
    self: `${baseUrl}/${userId}`,
    orders: `${baseUrl}/${userId}/orders`,
    update: `${baseUrl}/${userId}`,
    delete: `${baseUrl}/${userId}`
  };
};

exports.getAllUsers = async (req, res, next) => {
  try {
    // Get query parameters
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const skip = (page - 1) * limit;
    
    // Get users with pagination
    const [users, total] = await Promise.all([
      User.find().skip(skip).limit(limit),
      User.countDocuments()
    ]);
    
    // Calculate pagination data
    const totalPages = Math.ceil(total / limit);
    
    // Build pagination links
    const baseUrl = `${req.protocol}://${req.get('host')}/api/users`;
    const links = {
      self: `${baseUrl}?page=${page}&limit=${limit}`,
      first: `${baseUrl}?page=1&limit=${limit}`,
      last: `${baseUrl}?page=${totalPages}&limit=${limit}`
    };
    
    if (page > 1) {
      links.prev = `${baseUrl}?page=${page - 1}&limit=${limit}`;
    }
    
    if (page < totalPages) {
      links.next = `${baseUrl}?page=${page + 1}&limit=${limit}`;
    }
    
    // Send response
    res.status(200).json({
      data: users,
      meta: {
        pagination: { page, limit, total, totalPages }
      },
      links
    });
  } catch (error) {
    next(error);
  }
};

exports.getUserById = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    
    if (!user) {
      return next(new AppError('User not found', 404, 'NOT_FOUND'));
    }
    
    // Build links
    const links = buildUserLinks(req, user.id);
    
    res.status(200).json({
      data: user,
      links
    });
  } catch (error) {
    next(error);
  }
};

exports.createUser = async (req, res, next) => {
  try {
    const newUser = await User.create(req.body);
    
    // Build links
    const links = buildUserLinks(req, newUser.id);
    
    res.status(201).json({
      data: newUser,
      links
    });
  } catch (error) {
    next(error);
  }
};

exports.updateUser = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    
    if (!user) {
      return next(new AppError('User not found', 404, 'NOT_FOUND'));
    }
    
    // Build links
    const links = buildUserLinks(req, user.id);
    
    res.status(200).json({
      data: user,
      links
    });
  } catch (error) {
    next(error);
  }
};

exports.updateUserPartially = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      { $set: req.body },
      { new: true, runValidators: true }
    );
    
    if (!user) {
      return next(new AppError('User not found', 404, 'NOT_FOUND'));
    }
    
    // Build links
    const links = buildUserLinks(req, user.id);
    
    res.status(200).json({
      data: user,
      links
    });
  } catch (error) {
    next(error);
  }
};

exports.deleteUser = async (req, res, next) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    
    if (!user) {
      return next(new AppError('User not found', 404, 'NOT_FOUND'));
    }
    
    res.status(204).send();
  } catch (error) {
    next(error);
  }
};

exports.getUserOrders = async (req, res, next) => {
  try {
    // Check if user exists
    const user = await User.findById(req.params.id);
    
    if (!user) {
      return next(new AppError('User not found', 404, 'NOT_FOUND'));
    }
    
    // Get user orders
    const orders = await Order.find({ user: req.params.id });
    
    // Build links
    const links = {
      self: `${req.protocol}://${req.get('host')}/api/users/${req.params.id}/orders`,
      user: `${req.protocol}://${req.get('host')}/api/users/${req.params.id}`
    };
    
    res.status(200).json({
      data: orders,
      links
    });
  } catch (error) {
    next(error);
  }
};
            

Real-world example: The Express-based API that powers Airbnb follows these RESTful design principles. Their API uses resource-oriented URLs like /listings/:id and /users/:id/reviews, consistent HTTP methods, appropriate status codes, and includes links to related resources in responses. This approach allows them to handle millions of listings and bookings while providing a consistent, intuitive interface for both web and mobile clients.

Practice Activities

Activity 1: Basic RESTful API

Create a RESTful API for a blog with the following features:

  • Resource endpoints for posts and comments
  • Proper use of HTTP methods for CRUD operations
  • Appropriate status codes for different scenarios
  • Query parameters for filtering and pagination
  • Consistent response format for success and errors

Test your API using tools like Postman or curl.

Activity 2: Advanced RESTful Features

Enhance your blog API with more advanced RESTful features:

  • HATEOAS links in responses
  • Versioning with at least two versions of endpoints
  • Content negotiation (JSON and XML formats)
  • Authentication for protected resources
  • Rate limiting for API endpoints

Create a simple client that consumes your API to demonstrate its functionality.

Activity 3: Complete RESTful API Project

Build a complete RESTful API for an e-commerce system with:

  • Multiple related resources (products, categories, users, orders)
  • Nested resources and relationships
  • Full CRUD operations with proper validations
  • Search, filter, sort, and pagination capabilities
  • Comprehensive error handling
  • Authentication and authorization
  • API documentation using Swagger or a similar tool

Implement middleware for logging, response formatting, and security.

Key Takeaways

In the next lecture, we'll explore request validation and sanitization to ensure data integrity in your RESTful APIs.