Building a REST API Example
Now let's build a complete REST API example - a simple Task Management API. This will demonstrate all the REST principles we learned in the previous section.
Project Overview
We'll create a task management API that allows users to:
- Create, read, update, and delete tasks
- Organize tasks by categories
- Mark tasks as complete or incomplete
Project Structure
task-api/
├── package.json
├── server.js
├── routes/
│ ├── tasks.js
│ └── categories.js
└── data/
└── tasks.json
Step 1: Initialize the Project
Create a new directory and initialize the project:
mkdir task-api
cd task-api
npm init -y
Install required dependencies:
npm install express cors
npm install --save-dev nodemon
Step 2: Create package.json Scripts
{
"name": "task-api",
"version": "1.0.0",
"description": "A RESTful Task Management API",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
Step 3: Create the Main Server
server.js
const express = require('express');
const cors = require('cors');
const taskRoutes = require('./routes/tasks');
const categoryRoutes = require('./routes/categories');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/tasks', taskRoutes);
app.use('/api/categories', categoryRoutes);
// Root endpoint
app.get('/', (req, res) => {
res.json({
message: 'Task Management API',
version: '1.0.0',
endpoints: {
tasks: '/api/tasks',
categories: '/api/categories'
}
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
error: 'Endpoint not found',
message: `The endpoint ${req.method} ${req.originalUrl} does not exist`
});
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal server error',
message: 'Something went wrong on the server'
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Step 4: Create Sample Data
data/tasks.json
{
"tasks": [
{
"id": 1,
"title": "Learn REST API principles",
"description": "Study REST architecture and build examples",
"completed": false,
"category": "learning",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
},
{
"id": 2,
"title": "Buy groceries",
"description": "Milk, bread, eggs, and vegetables",
"completed": true,
"category": "personal",
"createdAt": "2024-01-14T15:30:00Z",
"updatedAt": "2024-01-15T09:00:00Z"
},
{
"id": 3,
"title": "Review project proposal",
"description": "Go through the Q1 project proposal and provide feedback",
"completed": false,
"category": "work",
"createdAt": "2024-01-13T14:20:00Z",
"updatedAt": "2024-01-13T14:20:00Z"
}
],
"categories": [
{
"id": "work",
"name": "Work",
"description": "Professional tasks and projects"
},
{
"id": "personal",
"name": "Personal",
"description": "Personal tasks and errands"
},
{
"id": "learning",
"name": "Learning",
"description": "Educational and skill development tasks"
}
]
}
Step 5: Create Task Routes
routes/tasks.js
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const router = express.Router();
const dataPath = path.join(__dirname, '../data/tasks.json');
// Helper function to read data
async function readData() {
try {
const data = await fs.readFile(dataPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error('Error reading data:', error);
return { tasks: [], categories: [] };
}
}
// Helper function to write data
async function writeData(data) {
try {
await fs.writeFile(dataPath, JSON.stringify(data, null, 2));
} catch (error) {
console.error('Error writing data:', error);
throw error;
}
}
// GET /api/tasks - Get all tasks
router.get('/', async (req, res) => {
try {
const data = await readData();
const { completed, category } = req.query;
let filteredTasks = data.tasks;
// Filter by completion status
if (completed !== undefined) {
const isCompleted = completed === 'true';
filteredTasks = filteredTasks.filter(task => task.completed === isCompleted);
}
// Filter by category
if (category) {
filteredTasks = filteredTasks.filter(task => task.category === category);
}
res.json({
success: true,
count: filteredTasks.length,
data: filteredTasks
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch tasks'
});
}
});
// GET /api/tasks/:id - Get a specific task
router.get('/:id', async (req, res) => {
try {
const data = await readData();
const taskId = parseInt(req.params.id);
const task = data.tasks.find(t => t.id === taskId);
if (!task) {
return res.status(404).json({
success: false,
error: 'Task not found'
});
}
res.json({
success: true,
data: task
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch task'
});
}
});
// POST /api/tasks - Create a new task
router.post('/', async (req, res) => {
try {
const data = await readData();
const { title, description, category } = req.body;
// Validation
if (!title || !title.trim()) {
return res.status(400).json({
success: false,
error: 'Title is required'
});
}
// Check if category exists
if (category && !data.categories.find(c => c.id === category)) {
return res.status(400).json({
success: false,
error: 'Invalid category'
});
}
// Create new task
const newTask = {
id: Math.max(...data.tasks.map(t => t.id), 0) + 1,
title: title.trim(),
description: description ? description.trim() : '',
completed: false,
category: category || 'personal',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
data.tasks.push(newTask);
await writeData(data);
res.status(201).json({
success: true,
data: newTask
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create task'
});
}
});
// PUT /api/tasks/:id - Update an entire task
router.put('/:id', async (req, res) => {
try {
const data = await readData();
const taskId = parseInt(req.params.id);
const taskIndex = data.tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({
success: false,
error: 'Task not found'
});
}
const { title, description, completed, category } = req.body;
// Validation
if (!title || !title.trim()) {
return res.status(400).json({
success: false,
error: 'Title is required'
});
}
// Check if category exists
if (category && !data.categories.find(c => c.id === category)) {
return res.status(400).json({
success: false,
error: 'Invalid category'
});
}
// Update task
data.tasks[taskIndex] = {
...data.tasks[taskIndex],
title: title.trim(),
description: description ? description.trim() : '',
completed: completed !== undefined ? completed : data.tasks[taskIndex].completed,
category: category || data.tasks[taskIndex].category,
updatedAt: new Date().toISOString()
};
await writeData(data);
res.json({
success: true,
data: data.tasks[taskIndex]
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to update task'
});
}
});
// PATCH /api/tasks/:id - Partially update a task
router.patch('/:id', async (req, res) => {
try {
const data = await readData();
const taskId = parseInt(req.params.id);
const taskIndex = data.tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({
success: false,
error: 'Task not found'
});
}
const updates = req.body;
// Check if category exists
if (updates.category && !data.categories.find(c => c.id === updates.category)) {
return res.status(400).json({
success: false,
error: 'Invalid category'
});
}
// Update only provided fields
Object.keys(updates).forEach(key => {
if (updates[key] !== undefined) {
if (key === 'title' && updates[key].trim()) {
data.tasks[taskIndex][key] = updates[key].trim();
} else if (key !== 'title') {
data.tasks[taskIndex][key] = updates[key];
}
}
});
data.tasks[taskIndex].updatedAt = new Date().toISOString();
await writeData(data);
res.json({
success: true,
data: data.tasks[taskIndex]
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to update task'
});
}
});
// DELETE /api/tasks/:id - Delete a task
router.delete('/:id', async (req, res) => {
try {
const data = await readData();
const taskId = parseInt(req.params.id);
const taskIndex = data.tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({
success: false,
error: 'Task not found'
});
}
const deletedTask = data.tasks[taskIndex];
data.tasks.splice(taskIndex, 1);
await writeData(data);
res.status(204).send();
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to delete task'
});
}
});
module.exports = router;
Step 6: Create Category Routes
routes/categories.js
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const router = express.Router();
const dataPath = path.join(__dirname, '../data/tasks.json');
// Helper functions (same as in tasks.js)
async function readData() {
try {
const data = await fs.readFile(dataPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error('Error reading data:', error);
return { tasks: [], categories: [] };
}
}
async function writeData(data) {
try {
await fs.writeFile(dataPath, JSON.stringify(data, null, 2));
} catch (error) {
console.error('Error writing data:', error);
throw error;
}
}
// GET /api/categories - Get all categories
router.get('/', async (req, res) => {
try {
const data = await readData();
res.json({
success: true,
count: data.categories.length,
data: data.categories
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch categories'
});
}
});
// GET /api/categories/:id - Get a specific category
router.get('/:id', async (req, res) => {
try {
const data = await readData();
const category = data.categories.find(c => c.id === req.params.id);
if (!category) {
return res.status(404).json({
success: false,
error: 'Category not found'
});
}
res.json({
success: true,
data: category
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch category'
});
}
});
// POST /api/categories - Create a new category
router.post('/', async (req, res) => {
try {
const data = await readData();
const { id, name, description } = req.body;
// Validation
if (!id || !name || !id.trim() || !name.trim()) {
return res.status(400).json({
success: false,
error: 'ID and name are required'
});
}
// Check if category already exists
if (data.categories.find(c => c.id === id.trim())) {
return res.status(409).json({
success: false,
error: 'Category already exists'
});
}
// Create new category
const newCategory = {
id: id.trim(),
name: name.trim(),
description: description ? description.trim() : ''
};
data.categories.push(newCategory);
await writeData(data);
res.status(201).json({
success: true,
data: newCategory
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create category'
});
}
});
module.exports = router;
Step 7: Test the API
Start the server:
npm run dev
Test the API endpoints:
1. Get all tasks:
curl http://localhost:3000/api/tasks
2. Get a specific task:
curl http://localhost:3000/api/tasks/1
3. Create a new task:
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test REST API", "description": "Testing our new API", "category": "learning"}'
4. Update a task:
curl -X PUT http://localhost:3000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Updated Task", "description": "This task was updated", "completed": true, "category": "work"}'
5. Mark task as complete:
curl -X PATCH http://localhost:3000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
6. Delete a task:
curl -X DELETE http://localhost:3000/api/tasks/1
7. Get tasks by category:
curl http://localhost:3000/api/tasks?category=work
8. Get completed tasks:
curl http://localhost:3000/api/tasks?completed=true
REST Principles Demonstrated
This example demonstrates all the key REST principles:
- Stateless: Each request contains all necessary information
- Resource-based URLs:
/api/tasks,/api/categories - HTTP Methods: GET, POST, PUT, PATCH, DELETE used appropriately
- Status Codes: 200, 201, 204, 400, 404, 409, 500
- JSON Representation: Data exchanged as JSON
- Uniform Interface: Consistent response format
API Response Format
All responses follow a consistent format:
{
"success": true,
"data": { ... },
"count": 5,
"error": "Error message"
}
Next Steps
- Add authentication and authorization
- Implement data validation middleware
- Add pagination for large datasets
- Include API documentation with Swagger
- Add rate limiting
- Implement proper error handling
- Add logging and monitoring
This REST API example provides a solid foundation for understanding and implementing RESTful web services!