Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Building a REST API
APIs (Application Programming Interfaces) are the backbone of modern web development. They allow different applications to communicate with each other—whether it’s your frontend talking to your backend, or third-party services integrating with your app.
Think of a REST API like a restaurant menu. The menu (API documentation) tells you what dishes (resources) are available and how to order them (HTTP methods). You do not need to know how the kitchen works internally—you just place an order (request) and receive your dish (response). The waiter (HTTP) carries messages back and forth using a standardized process (the REST constraints).
What is REST?
REST (Representational State Transfer) is an architectural style for designing networked applications. It’s not a protocol or standard, but a set of constraints that, when followed, create scalable and maintainable APIs.
Core Principles of REST
| Principle | Description |
|---|
| Stateless | Each request contains all the information needed to process it. The server doesn’t store client state between requests. |
| Client-Server | The client and server are separate, allowing them to evolve independently. |
| Uniform Interface | Resources are accessed using standard HTTP methods (GET, POST, PUT, DELETE). |
| Resource-Based | Everything is a resource (users, products, orders) identified by URLs. |
HTTP Methods and Their Meanings
| Method | Purpose | Example |
|---|
GET | Retrieve data | Get all users, get user by ID |
POST | Create new data | Create a new user |
PUT | Update existing data (replace) | Update a user’s information |
PATCH | Partial update | Update only user’s email |
DELETE | Remove data | Delete a user |
Why REST?
- Universal: Works with any programming language that can make HTTP requests
- Scalable: Statelessness allows easy horizontal scaling
- Cacheable: GET requests can be cached for better performance
- Simple: Uses familiar HTTP concepts developers already know
In this chapter, we will build a simple REST API to manage a list of items, demonstrating these principles in action.
Setup
Create a new file server.js and install express if you haven’t.
const express = require('express');
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies
const PORT = process.env.PORT || 5000;
// Mock Database -- In a real application, this would be a database like
// PostgreSQL or MongoDB. Using an in-memory array means all data is lost
// when the server restarts. This is fine for learning, but never for production.
let items = [
{ id: 1, name: 'Item One' },
{ id: 2, name: 'Item Two' }
];
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
GET: Read Data
// Get all items
app.get('/api/items', (req, res) => {
res.json(items);
});
// Get single item
app.get('/api/items/:id', (req, res) => {
const found = items.some(item => item.id === parseInt(req.params.id));
if (found) {
res.json(items.filter(item => item.id === parseInt(req.params.id)));
} else {
res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
}
});
POST: Create Data
app.post('/api/items', (req, res) => {
const newItem = {
id: items.length + 1, // Simple ID generation -- WARNING: this breaks after deletions!
// If you delete item 2 from a 3-item list and add a new one,
// you get id=3 which collides with the existing item 3.
// In production, use UUIDs or database auto-increment.
name: req.body.name
};
if (!newItem.name) {
return res.status(400).json({ msg: 'Please include a name' });
}
items.push(newItem);
res.status(201).json(newItem); // Return the created item with 201 status
});
PUT: Update Data
app.put('/api/items/:id', (req, res) => {
const found = items.some(item => item.id === parseInt(req.params.id));
if (found) {
const updItem = req.body;
items.forEach(item => {
if (item.id === parseInt(req.params.id)) {
item.name = updItem.name ? updItem.name : item.name;
res.json({ msg: 'Item updated', item });
}
});
} else {
res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
}
});
DELETE: Remove Data
app.delete('/api/items/:id', (req, res) => {
const found = items.some(item => item.id === parseInt(req.params.id));
if (found) {
items = items.filter(item => item.id !== parseInt(req.params.id));
res.json({ msg: 'Item deleted', items });
} else {
res.status(404).json({ msg: `No item with the id of ${req.params.id}` });
}
});
Testing with Postman / Insomnia
Since we don’t have a frontend yet, use tools like Postman or Insomnia to test your API.
- GET
http://localhost:5000/api/items -> Should return list.
- POST
http://localhost:5000/api/items with JSON body {"name": "New Item"} -> Should add item.
- PUT
http://localhost:5000/api/items/1 with JSON body {"name": "Updated Item"} -> Should update item 1.
- DELETE
http://localhost:5000/api/items/1 -> Should delete item 1.
Summary
- REST APIs use standard HTTP methods for CRUD operations
- GET for retrieving data (should never modify server state)
- POST for creating data (return 201, not 200)
- PUT for full replacement, PATCH for partial update
- DELETE for removing data (return 204 No Content for success)
- Always validate input and handle errors—never trust data from the client
- Use proper HTTP status codes: they are part of the API contract, not decoration
Request Validation with Joi
Never trust client input! Validate all requests:
const Joi = require('joi');
// Define schema
const itemSchema = Joi.object({
name: Joi.string().min(3).max(50).required(),
description: Joi.string().max(500),
price: Joi.number().positive().precision(2),
category: Joi.string().valid('electronics', 'clothing', 'food'),
tags: Joi.array().items(Joi.string()),
inStock: Joi.boolean().default(true)
});
// Validation middleware
const validate = (schema) => (req, res, next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({ success: false, errors });
}
req.body = value; // Use validated/sanitized values
next();
};
// Usage
app.post('/api/items', validate(itemSchema), (req, res) => {
// req.body is now validated and sanitized
res.status(201).json(req.body);
});
API Response Standards
Maintain consistent response formats:
// Success responses
const sendSuccess = (res, data, statusCode = 200) => {
res.status(statusCode).json({
success: true,
data
});
};
// Paginated responses
const sendPaginated = (res, data, page, limit, total) => {
res.json({
success: true,
data,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
};
// Error responses
const sendError = (res, message, statusCode = 400) => {
res.status(statusCode).json({
success: false,
error: message
});
};
app.get('/api/items', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Database query with pagination
const items = await Item.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await Item.countDocuments();
res.json({
success: true,
data: items,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
});
Filtering and Sorting
app.get('/api/items', async (req, res) => {
// Build filter object
const filter = {};
if (req.query.category) {
filter.category = req.query.category;
}
if (req.query.minPrice) {
filter.price = { ...filter.price, $gte: parseFloat(req.query.minPrice) };
}
if (req.query.maxPrice) {
filter.price = { ...filter.price, $lte: parseFloat(req.query.maxPrice) };
}
if (req.query.search) {
filter.name = { $regex: req.query.search, $options: 'i' };
}
// Build sort object
const sort = {};
if (req.query.sortBy) {
const order = req.query.order === 'desc' ? -1 : 1;
sort[req.query.sortBy] = order;
}
const items = await Item.find(filter).sort(sort);
res.json({ success: true, data: items });
});
// Example: GET /api/items?category=electronics&minPrice=100&sortBy=price&order=desc
HTTP Status Codes Reference
| Code | Name | When to Use |
|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input, validation error |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | Duplicate entry, version conflict |
| 422 | Unprocessable Entity | Valid JSON but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error |
API Versioning
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Or using headers
app.use('/api', (req, res, next) => {
const version = req.headers['api-version'] || 'v1';
req.apiVersion = version;
next();
});
Complete REST Controller Example
// controllers/itemController.js
class ItemController {
// GET /api/items
async getAll(req, res, next) {
try {
const { page = 1, limit = 10, sort = '-createdAt' } = req.query;
const items = await Item.find()
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit));
const total = await Item.countDocuments();
res.json({
success: true,
count: items.length,
data: items,
pagination: { page: parseInt(page), limit: parseInt(limit), total }
});
} catch (error) {
next(error);
}
}
// GET /api/items/:id
async getById(req, res, next) {
try {
const item = await Item.findById(req.params.id);
if (!item) {
return res.status(404).json({
success: false,
error: 'Item not found'
});
}
res.json({ success: true, data: item });
} catch (error) {
next(error);
}
}
// POST /api/items
async create(req, res, next) {
try {
const item = await Item.create(req.body);
res.status(201).json({ success: true, data: item });
} catch (error) {
next(error);
}
}
// PUT /api/items/:id
async update(req, res, next) {
try {
const item = await Item.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!item) {
return res.status(404).json({
success: false,
error: 'Item not found'
});
}
res.json({ success: true, data: item });
} catch (error) {
next(error);
}
}
// DELETE /api/items/:id
async delete(req, res, next) {
try {
const item = await Item.findByIdAndDelete(req.params.id);
if (!item) {
return res.status(404).json({
success: false,
error: 'Item not found'
});
}
res.status(204).send();
} catch (error) {
next(error);
}
}
}
module.exports = new ItemController();