Building CRUD Endpoints
CRUD stands for Create, Read, Update, and Delete - the four basic operations for managing data. In this lesson, you'll build a complete set of CRUD endpoints for a REST API using Node.js.
CRUD Operations Overview
| Operation | HTTP Method | Endpoint | Description |
|---|---|---|---|
| Create | POST | /items | Create new item |
| Read (all) | GET | /items | Get all items |
| Read (one) | GET | /items/:id | Get one item |
| Update | PUT | /items/:id | Replace item |
| Update | PATCH | /items/:id | Partial update |
| Delete | DELETE | /items/:id | Remove item |
Setting Up the Server
First, let's create a basic HTTP server structure:
const http = require('http');
const server = http.createServer((req, res) => {
const { method, url } = req;
// Set JSON content type
res.setHeader('Content-Type', 'application/json');
// Route handling goes here
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
In-Memory Data Store
For learning, we'll use an in-memory array as our data store:
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
let nextId = 3;
Implementing CREATE (POST)
// POST /users
if (method === 'POST' && url === '/users') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const userData = JSON.parse(body);
const newUser = {
id: nextId++,
...userData,
createdAt: new Date().toISOString()
};
users.push(newUser);
res.statusCode = 201;
res.end(JSON.stringify(newUser));
});
return;
}
Implementing READ (GET)
Get All Items
// GET /users
if (method === 'GET' && url === '/users') {
res.statusCode = 200;
res.end(JSON.stringify(users));
return;
}
Get Single Item
// GET /users/:id
if (method === 'GET' && url.match(/^\/users\/\d+$/)) {
const id = parseInt(url.split('/')[2]);
const user = users.find(u => u.id === id);
if (!user) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.statusCode = 200;
res.end(JSON.stringify(user));
return;
}
Implementing UPDATE (PUT/PATCH)
PUT - Replace Entire Resource
// PUT /users/:id
if (method === 'PUT' && url.match(/^\/users\/\d+$/)) {
const id = parseInt(url.split('/')[2]);
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const index = users.findIndex(u => u.id === id);
if (index === -1) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
const userData = JSON.parse(body);
users[index] = { id, ...userData, updatedAt: new Date().toISOString() };
res.statusCode = 200;
res.end(JSON.stringify(users[index]));
});
return;
}
PATCH - Partial Update
// PATCH /users/:id
if (method === 'PATCH' && url.match(/^\/users\/\d+$/)) {
// Similar to PUT but merges with existing data
users[index] = { ...users[index], ...userData, updatedAt: new Date() };
}
Implementing DELETE
// DELETE /users/:id
if (method === 'DELETE' && url.match(/^\/users\/\d+$/)) {
const id = parseInt(url.split('/')[2]);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
users.splice(index, 1);
res.statusCode = 204; // No Content
res.end();
return;
}
Complete CRUD Router
Request Validation
Always validate incoming data:
function validateUser(data) {
const errors = [];
if (!data.name || data.name.trim() === '') {
errors.push('Name is required');
}
if (!data.email) {
errors.push('Email is required');
} else if (!data.email.includes('@')) {
errors.push('Invalid email format');
}
return errors;
}
// In route handler
const errors = validateUser(userData);
if (errors.length > 0) {
res.statusCode = 400;
res.end(JSON.stringify({ errors }));
return;
}
Error Response Format
Consistent error responses make APIs easier to use:
function sendError(res, status, code, message, details = null) {
res.statusCode = status;
res.end(JSON.stringify({
error: {
code,
message,
...(details && { details })
}
}));
}
// Usage
sendError(res, 404, 'USER_NOT_FOUND', 'User with ID 123 not found');
sendError(res, 400, 'VALIDATION_ERROR', 'Invalid input', [
{ field: 'email', message: 'Invalid email format' }
]);
Pagination
For large collections, implement pagination:
// GET /users?page=2&limit=10
function paginate(items, page = 1, limit = 10) {
const start = (page - 1) * limit;
const end = start + limit;
const paginatedItems = items.slice(start, end);
return {
data: paginatedItems,
meta: {
total: items.length,
page,
limit,
pages: Math.ceil(items.length / limit)
}
};
}
Filtering and Sorting
// GET /products?category=electronics&sort=price&order=desc
function filterAndSort(items, query) {
let result = [...items];
// Filter
if (query.category) {
result = result.filter(item => item.category === query.category);
}
// Sort
if (query.sort) {
result.sort((a, b) => {
const order = query.order === 'desc' ? -1 : 1;
if (a[query.sort] < b[query.sort]) return -1 * order;
if (a[query.sort] > b[query.sort]) return 1 * order;
return 0;
});
}
return result;
}
Key Takeaways
- CRUD maps to HTTP methods: POST, GET, PUT/PATCH, DELETE
- Always validate input data before processing
- Return appropriate status codes for each operation
- Use consistent error response formats
- Implement pagination for large collections
- Add filtering and sorting with query parameters
- Return 404 when resources are not found
- Use 201 for successful creation, 204 for successful deletion
Summary
You've learned how to build complete CRUD endpoints for a REST API. You understand how to handle each HTTP method, validate input, return appropriate status codes, and structure responses consistently. These patterns form the foundation for building robust APIs.
Next, you'll learn about middleware patterns that help organize and reuse code across your endpoints.

