13 Crud Endpoints
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
- Returning 200 for all successful operations — Use 201 for creation and 204 for deletion. Different status codes give clients meaningful information.
- Not returning the created resource — After POST, return the created resource with its server-generated ID so clients can reference it.
- 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.
- Exposing internal IDs in responses — Use UUIDs instead of auto-increment integers for resource IDs to prevent information leakage and enumeration attacks.
- 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
- What status code should a successful POST return?
- What is the difference between PUT and PATCH?
- Why should you return the created resource in a POST response?
- How do you handle pagination in list endpoints?
- 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
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