Skip to content

Mean 08 Rest Api Routes

DodaTech 6 min read

title: "REST API Routes — Exposing CRUD Operations via Express" description: "Build RESTful API routes in Express for the MEAN Stack with CRUD endpoints, route parameters, query strings, and proper HTTP status codes." weight: 18 date: 2026-06-28 lastmod: 2026-06-28 tags: [frameworks, mean]

REST API routes connect the Express server to Mongoose models, exposing CRUD operations as HTTP endpoints that the Angular frontend consumes.

What You'll Learn

You will build RESTful API routes with proper HTTP methods, status codes, route parameters, query filtering, and error handling.

Why It Matters

Well-designed REST APIs are consistent, predictable, and easy to consume from Angular. Proper status codes and error formats simplify frontend error handling.

Real-World Use

DodaZIP's file management API follows REST conventions with standardized response formats, pagination metadata, and consistent error structures.

flowchart LR
    A[HTTP Request] --> B[Route Handler]
    B --> C{HTTP Method}
    C -->|GET| D[Read Data]
    C -->|POST| E[Create Data]
    C -->|PUT| F[Update Data]
    C -->|DELETE| G[Delete Data]
    D --> H[JSON Response]
    E --> H
    F --> H
    G --> H
    style B fill:#4a90d9,color:#fff

Basic CRUD Routes

Create a complete set of CRUD routes for a resource.

// backend/routes/productRoutes.js
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');

// GET /api/products — List all with pagination
router.get('/', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;
    const filter = {};

    if (req.query.category) filter.category = req.query.category;
    if (req.query.minPrice) filter.price = { $gte: parseFloat(req.query.minPrice) };

    const [products, total] = await Promise.all([
      Product.find(filter).skip(skip).limit(limit).sort({ createdAt: -1 }),
      Product.countDocuments(filter)
    ]);

    res.json({
      products,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// GET /api/products/:id — Get single product
router.get('/:id', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.json(product);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// POST /api/products — Create product
router.post('/', async (req, res) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json(product);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// PUT /api/products/:id — Update product
router.put('/:id', async (req, res) => {
  try {
    const product = await Product.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.json(product);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// DELETE /api/products/:id — Delete product
router.delete('/:id', async (req, res) => {
  try {
    const product = await Product.findByIdAndDelete(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

Expected output: A complete REST API for products with paginated listing, single item retrieval, creation, update, and deletion. Each endpoint returns appropriate status codes and JSON responses.

Mounting Routes in the Server

Mount route modules in the main server file.

// backend/server.js
const productRoutes = require('./routes/productRoutes');
const userRoutes = require('./routes/userRoutes');
const orderRoutes = require('./routes/orderRoutes');

app.use('/api/products', productRoutes);
app.use('/api/users', userRoutes);
app.use('/api/orders', orderRoutes);

Expected output: All route modules are mounted on their base paths. GET /api/products routes to the product list handler. POST /api/users routes to the user creation handler.

Response Format Standardization

Create a consistent response format for all endpoints.

// backend/utils/response.js
function success(res, data, statusCode = 200) {
  return res.status(statusCode).json({
    success: true,
    data
  });
}

function paginated(res, data, pagination, statusCode = 200) {
  return res.status(statusCode).json({
    success: true,
    data,
    pagination
  });
}

function error(res, message, statusCode = 500) {
  return res.status(statusCode).json({
    success: false,
    error: message
  });
}

module.exports = { success, paginated, error };

Usage:

const { success, paginated, error } = require('../utils/response');

router.get('/:id', async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return error(res, 'Product not found', 404);
    }
    return success(res, product);
  } catch (err) {
    return error(res, err.message);
  }
});

Expected output: All endpoints return a consistent JSON structure with success flag, data or error field, and optional pagination metadata.

Error Handling in Routes

Each route handler should handle specific error types.

router.get('/:id', async (req, res) => {
  try {
    // Validate ObjectId format
    if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
      return res.status(400).json({ error: 'Invalid product ID format' });
    }

    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }

    res.json(product);
  } catch (error) {
    // Handle CastError (invalid ObjectId)
    if (error.name === 'CastError') {
      return res.status(400).json({ error: 'Invalid ID format' });
    }
    // Handle ValidationError
    if (error.name === 'ValidationError') {
      return res.status(400).json({ error: error.message });
    }
    // Generic server error
    res.status(500).json({ error: 'Internal server error' });
  }
});

Expected output: Specific error types return appropriate HTTP status codes and messages. Invalid IDs return 400. Missing resources return 404. Server errors return 500.

Common Mistakes

  1. Not validating ObjectId format before querying: Invalid ID strings cause CastError. Check with mongoose.Types.ObjectId.isValid().

  2. Forgetting runValidators on update: Updates skip schema validation by default. Add { runValidators: true } to update options.

  3. Not handling the 204 response correctly: DELETE endpoints return 204 with no body. Do not try to send JSON with a 204 response.

  4. Exposing internal error messages in production: Map error messages to user-friendly responses. Do not leak stack traces.

  5. Not implementing pagination for list endpoints: Without pagination, endpoints return all documents, causing performance issues as data grows.

Practice Questions

  1. What is the correct HTTP method for creating a resource?

POST. It creates a new resource and returns 201 status code with the created resource.

  1. What status code should a DELETE endpoint return?

204 No Content. The resource is deleted and there is no response body.

  1. How do you validate an ObjectId parameter in Express?

Use mongoose.Types.ObjectId.isValid(req.params.id). Return 400 if invalid.

  1. What does runValidators: true do in update operations?

It runs schema validation on the update data. Without it, updates can bypass required field and type validations.

  1. How do you implement pagination in a REST endpoint?

Use req.query.page and req.query.limit. Calculate skip = (page - 1) * limit. Return data with pagination metadata.

Challenge

Build a complete REST API for an e-commerce application with products, categories, and orders. Each resource should have full CRUD with pagination, filtering, sorting, and consistent error handling.

Frequently Asked Questions

Should I use PUT or PATCH for updates?

PUT replaces the entire resource. PATCH applies partial updates. Use PUT for full updates and PATCH for partial updates.

How do I handle nested resources routes?

Use nested routes like /api/products/:productId/reviews. Create a review router that reads the productId parameter.

What is the difference between 400 and 422 status codes?

400 is generic bad request. 422 is specifically for validation errors. Both are acceptable for validation failures.

How do I secure API routes?

Use authentication middleware that verifies JWT tokens before the route handler runs. Return 401 for unauthenticated requests.

Should I wrap all route handlers in try/catch?

Yes. Use a wrapper function that catches errors and passes them to the error handling middleware.

Mini Project

Build a REST API for a blog with posts and comments. Posts should have CRUD with pagination and filtering by status. Comments should be nested under posts.

What's Next

Set up Angular Setup MEAN to consume the REST API from the frontend.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro