Skip to content

13 Crud Endpoints

DodaTech 6 min read

title: CRUD Endpoints in Node.js REST APIs — Complete Guide weight: 23 date: 2026-06-28 lastmod: 2026-06-28 description: Learn building CRUD endpoints for Node.js REST APIs with Express, including create, read, update, and delete operations with proper validation and error handling. tags: [api-development, nodejs]


CRUD endpoints form the foundation of Node.js REST APIs, with GET for reading, POST for creating, PUT/PATCH for updating, and DELETE for removing resources, each following REST conventions with proper status codes and validation.

```mermaid
flowchart TD
  A[CRUD Endpoints] --> B[Create POST]
  A --> C[Read GET]
  A --> D[Update PUT/PATCH]
  A --> E[Delete DELETE]
  B --> F[201 + Resource]
  C --> G[200 + Resource(s)]
  D --> H[200 + Updated]
  E --> I[204 No Content]
  style A fill:#e1f5fe
  style B fill:#c8e6c9
  style C fill:#c8e6c9
  style D fill:#fff9c4
  style E fill:#ffcdd2

A complete CRUD controller has five methods: list (GET all with pagination), getById (GET single), create (POST with validation), update (PUT/PATCH with partial data), and delete (DELETE with ownership check). Each method returns appropriate status codes and consistent response formats.

Think of CRUD like a library management system. Create adds a new book to the catalog. Read shows book details. Update corrects a mis-shelved book. Delete removes a withdrawn book. Each operation has specific rules and confirmation steps.

Example: Complete CRUD Controller

const Product = require('../models/Product');
const factory = require('./factoryController');

// GET /api/products
exports.listProducts = async (req, res, next) => {
  try {
    const { page = 1, limit = 10, sort = '-createdAt', ...filters } = req.query;

    const query = Product.find(filters)
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(limit);

    const [products, total] = await Promise.all([
      query,
      Product.countDocuments(filters)
    ]);

    res.json({
      status: 'success',
      data: products,
      meta: {
        total,
        page: parseInt(page),
        limit: parseInt(limit),
        totalPages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    next(error);
  }
};

// GET /api/products/:id
exports.getProduct = async (req, res, next) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.json({ status: 'success', data: product });
  } catch (error) {
    next(error);
  }
};

// POST /api/products
exports.createProduct = async (req, res, next) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json({ status: 'success', data: product });
  } catch (error) {
    next(error);
  }
};

// PATCH /api/products/:id
exports.updateProduct = async (req, res, next) => {
  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({ status: 'success', data: product });
  } catch (error) {
    next(error);
  }
};

// DELETE /api/products/:id
exports.deleteProduct = async (req, res, next) => {
  try {
    const product = await Product.findByIdAndDelete(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.status(204).end();
  } catch (error) {
    next(error);
  }
};

Example: Factory Pattern for CRUD

const factory = (Model, options = {}) => ({
  list: async (req, res, next) => {
    try {
      const { page, limit, sort, ...filters } = req.query;
      const docs = await Model.find(filters)
        .sort(sort || '-createdAt')
        .skip((page - 1) * limit)
        .limit(limit);
      const total = await Model.countDocuments(filters);
      res.json({ status: 'success', data: docs, meta: { total, page, limit } });
    } catch (error) { next(error); }
  },

  get: async (req, res, next) => {
    try {
      const doc = await Model.findById(req.params.id);
      if (!doc) return res.status(404).json({ error: 'Not found' });
      res.json({ status: 'success', data: doc });
    } catch (error) { next(error); }
  },

  create: async (req, res, next) => {
    try {
      const doc = await Model.create(req.body);
      res.status(201).json({ status: 'success', data: doc });
    } catch (error) { next(error); }
  },

  update: async (req, res, next) => {
    try {
      const doc = await Model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
      if (!doc) return res.status(404).json({ error: 'Not found' });
      res.json({ status: 'success', data: doc });
    } catch (error) { next(error); }
  },

  delete: async (req, res, next) => {
    try {
      const doc = await Model.findByIdAndDelete(req.params.id);
      if (!doc) return res.status(404).json({ error: 'Not found' });
      res.status(204).end();
    } catch (error) { next(error); }
  }
});

// Usage: const productController = factory(Product);
// router.get('/products', productController.list);

Example: Testing CRUD Endpoints

const request = require('supertest');
const app = require('../app');

describe('Products API', () => {
  let productId;

  test('POST /api/products - creates a product', async () => {
    const res = await request(app)
      .post('/api/products')
      .send({ name: 'Test Product', price: 29.99 });
    expect(res.status).toBe(201);
    expect(res.body.data.name).toBe('Test Product');
    productId = res.body.data._id;
  });

  test('GET /api/products - lists products', async () => {
    const res = await request(app).get('/api/products');
    expect(res.status).toBe(200);
    expect(res.body.data).toBeInstanceOf(Array);
  });

  test('DELETE /api/products/:id - deletes a product', async () => {
    const res = await request(app).delete(`/api/products/${productId}`);
    expect(res.status).toBe(204);
  });
});

Expected output:

PASS  tests/product.test.js
  Products API
    ✓ POST /api/products - creates a product (45 ms)
    ✓ GET /api/products - lists products (12 ms)
    ✓ DELETE /api/products/:id - deletes a product (8 ms)

Common Mistakes

  1. Returning 200 for all successful operations — Use 201 for creation and 204 for deletion. Different status codes give clients meaningful information.
  2. Not returning the created resource — After POST, return the created resource with its server-generated ID so clients can reference it.
  3. Using PUT when PATCH is appropriate — PUT replaces the entire resource. PATCH applies partial updates. Use PUT only when the client sends the complete resource.
  4. Exposing internal IDs in responses — Use UUIDs instead of auto-increment integers for resource IDs to prevent information leakage and enumeration attacks.
  5. Not handling concurrent updates — Two clients updating the same resource simultaneously can cause lost updates. Use optimistic locking with version numbers or timestamps.

Practice Questions

  1. What status code should a successful POST return?
  2. What is the difference between PUT and PATCH?
  3. Why should you return the created resource in a POST response?
  4. How do you handle pagination in list endpoints?
  5. Challenge: Build a complete CRUD API for a task management system with validation, pagination, sorting, and error handling. Include endpoints for creating, listing, getting, updating, and deleting tasks. Test all endpoints with supertest.

FAQ

Should I use Express Router or app methods for CRUD?

Use Express Router for modularity. Create a router for each resource and mount it on the app with a prefix: app.use('/api/products', productRouter).

How do I handle partial updates?

Use PATCH with a validation schema that makes all fields optional. Update only the fields present in the request body.

What if a resource to delete does not exist?

Return 404 Not Found. The resource cannot be deleted because it does not exist. This is different from successfully deleting an existing resource (204).

Should I return the deleted resource in the response?

No, return 204 No Content with no body. The resource is gone. If you need to return information about the deletion, use 200 with the deleted resource data.

How do I handle bulk CRUD operations?

Create separate bulk endpoints: POST /api/products/batch, PATCH /api/products/batch, DELETE /api/products/batch. Accept arrays and return per-item results.

Mini Project

Build a complete CRUD API for a notes application. Implement all five CRUD operations with pagination, sorting, and validation. Use a factory pattern to reduce boilerplate. Write integration tests for each endpoint with supertest.

What's Next

Now learn about filtering and sorting implementation in Building REST APIs with Node.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro