Building Your First REST API
Time to Build!
You've learned the theory behind REST APIs - HTTP methods, status codes, URL design, and data formats. Now it's time to put that knowledge into practice by building a real REST API from scratch.
What We'll Build
We're building a simple Task Management API with these features:
- Create, read, update, and delete tasks (CRUD operations)
- List all tasks with filtering and pagination
- Mark tasks as complete/incomplete
- Proper HTTP status codes and error handling
- JSON request/response format
Project Setup
Prerequisites:
- Node.js installed (version 14 or higher)
- Basic JavaScript knowledge
- A code editor (VS Code recommended)
- Terminal/Command Prompt access
Step 1: Initialize Project
# Create project directory
mkdir task-api
cd task-api
# Initialize npm project
npm init -y
# Install dependencies
npm install express
npm install --save-dev nodemon
# Create project structure
mkdir src
touch src/server.js
touch src/routes.js
Step 2: Configure Package.json
Update your package.json to include start scripts:
{
"name": "task-api",
"version": "1.0.0",
"description": "Simple REST API for task management",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
Building the Server
Step 3: Create Basic Server (src/server.js)
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Simple welcome route
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Task Management API',
version: '1.0.0',
endpoints: {
tasks: '/api/v1/tasks'
}
});
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
module.exports = app;
Step 4: Test Your Server
# Start the server
npm run dev
# Server should start on http://localhost:3000
# Visit http://localhost:3000 in your browser
# You should see the welcome JSON response
Creating the Task Model
Step 5: In-Memory Data Store
For simplicity, we'll use an in-memory array. In production, you'd use a database like MongoDB, PostgreSQL, or MySQL.
// Add to src/server.js after app initialization
// In-memory task storage (replace with database in production)
let tasks = [
{
id: 1,
title: 'Learn REST APIs',
description: 'Complete the REST API tutorial',
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: 2,
title: 'Build sample API',
description: 'Create a task management API',
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
];
// Counter for generating new IDs
let nextId = 3;
Implementing CRUD Endpoints
GET /api/v1/tasks - List All Tasks
// Get all tasks (with optional filtering)
app.get('/api/v1/tasks', (req, res) => {
const { completed, search } = req.query;
let filteredTasks = tasks;
// Filter by completion status
if (completed !== undefined) {
const isCompleted = completed === 'true';
filteredTasks = filteredTasks.filter(task => task.completed === isCompleted);
}
// Search in title and description
if (search) {
const searchLower = search.toLowerCase();
filteredTasks = filteredTasks.filter(task =>
task.title.toLowerCase().includes(searchLower) ||
task.description.toLowerCase().includes(searchLower)
);
}
res.json({
data: filteredTasks,
meta: {
total: filteredTasks.length,
filtered: completed !== undefined || search !== undefined
}
});
});
GET /api/v1/tasks/:id - Get Single Task
// Get single task by ID
app.get('/api/v1/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id);
const task = tasks.find(t => t.id === taskId);
if (!task) {
return res.status(404).json({
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
});
}
res.json({
data: task
});
});
POST /api/v1/tasks - Create New Task
// Create new task
app.post('/api/v1/tasks', (req, res) => {
const { title, description } = req.body;
// Validation
const errors = [];
if (!title || title.trim() === '') {
errors.push({
field: 'title',
message: 'Title is required'
});
}
if (title && title.length < 3) {
errors.push({
field: 'title',
message: 'Title must be at least 3 characters'
});
}
if (errors.length > 0) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: errors
}
});
}
// Create new task
const newTask = {
id: nextId++,
title: title.trim(),
description: description ? description.trim() : '',
completed: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
tasks.push(newTask);
res.status(201).json({
data: newTask
});
});
PUT /api/v1/tasks/:id - Update Entire Task
// Update entire task (PUT)
app.put('/api/v1/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id);
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
});
}
const { title, description, completed } = req.body;
// Validation
if (!title || title.trim() === '') {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Title is required'
}
});
}
// Update task (replace entire resource)
tasks[taskIndex] = {
id: taskId,
title: title.trim(),
description: description ? description.trim() : '',
completed: completed === true,
createdAt: tasks[taskIndex].createdAt, // Keep original creation date
updatedAt: new Date().toISOString()
};
res.json({
data: tasks[taskIndex]
});
});
PATCH /api/v1/tasks/:id - Partial Update
// Partially update task (PATCH)
app.patch('/api/v1/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id);
const task = tasks.find(t => t.id === taskId);
if (!task) {
return res.status(404).json({
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
});
}
// Update only provided fields
if (req.body.title !== undefined) {
if (req.body.title.trim() === '') {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Title cannot be empty'
}
});
}
task.title = req.body.title.trim();
}
if (req.body.description !== undefined) {
task.description = req.body.description.trim();
}
if (req.body.completed !== undefined) {
task.completed = req.body.completed === true;
}
task.updatedAt = new Date().toISOString();
res.json({
data: task
});
});
DELETE /api/v1/tasks/:id - Delete Task
// Delete task
app.delete('/api/v1/tasks/:id', (req, res) => {
const taskId = parseInt(req.params.id);
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex === -1) {
return res.status(404).json({
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
});
}
// Remove task from array
tasks.splice(taskIndex, 1);
// Return 204 No Content (no response body)
res.status(204).send();
});
Special Action Endpoint
POST /api/v1/tasks/:id/toggle - Toggle Completion
// Toggle task completion (special action)
app.post('/api/v1/tasks/:id/toggle', (req, res) => {
const taskId = parseInt(req.params.id);
const task = tasks.find(t => t.id === taskId);
if (!task) {
return res.status(404).json({
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
});
}
// Toggle completed status
task.completed = !task.completed;
task.updatedAt = new Date().toISOString();
res.json({
data: task,
meta: {
action: 'toggled',
newStatus: task.completed ? 'completed' : 'incomplete'
}
});
});
Error Handling Middleware
Add global error handling at the end of your server.js file:
// Handle 404 - Route not found
app.use((req, res) => {
res.status(404).json({
error: {
code: 'ENDPOINT_NOT_FOUND',
message: `Cannot ${req.method} ${req.path}`,
availableEndpoints: [
'GET /api/v1/tasks',
'GET /api/v1/tasks/:id',
'POST /api/v1/tasks',
'PUT /api/v1/tasks/:id',
'PATCH /api/v1/tasks/:id',
'DELETE /api/v1/tasks/:id',
'POST /api/v1/tasks/:id/toggle'
]
}
});
});
// Handle 500 - Server errors
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Something went wrong on the server',
...(process.env.NODE_ENV === 'development' && { details: err.message })
}
});
});
Testing Your API
Using cURL (Command Line):
# Get all tasks
curl http://localhost:3000/api/v1/tasks
# Get single task
curl http://localhost:3000/api/v1/tasks/1
# Create new task
curl -X POST http://localhost:3000/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{"title":"New Task","description":"Test task"}'
# Update task (partial)
curl -X PATCH http://localhost:3000/api/v1/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# Delete task
curl -X DELETE http://localhost:3000/api/v1/tasks/1
# Filter completed tasks
curl http://localhost:3000/api/v1/tasks?completed=true
# Search tasks
curl http://localhost:3000/api/v1/tasks?search=API
Using Postman:
- Download and install Postman (free tool)
- Create a new request
- Set method (GET, POST, PUT, PATCH, DELETE)
- Enter URL: http://localhost:3000/api/v1/tasks
- For POST/PUT/PATCH: Select "Body" → "raw" → "JSON"
- Enter JSON data and click Send
Using VS Code REST Client Extension:
Create a file test-api.http:
### Get all tasks
GET http://localhost:3000/api/v1/tasks
### Get single task
GET http://localhost:3000/api/v1/tasks/1
### Create new task
POST http://localhost:3000/api/v1/tasks
Content-Type: application/json
{
"title": "Learn REST APIs",
"description": "Complete tutorial and build API"
}
### Update task
PATCH http://localhost:3000/api/v1/tasks/1
Content-Type: application/json
{
"completed": true
}
### Delete task
DELETE http://localhost:3000/api/v1/tasks/1
API Documentation
Good APIs are well-documented. Here's a simple README for your API:
# Task Management API
Base URL: `http://localhost:3000/api/v1`
## Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /tasks | List all tasks |
| GET | /tasks/:id | Get single task |
| POST | /tasks | Create new task |
| PUT | /tasks/:id | Update entire task |
| PATCH | /tasks/:id | Partially update task |
| DELETE | /tasks/:id | Delete task |
| POST | /tasks/:id/toggle | Toggle completion |
## Query Parameters
- `completed`: Filter by status (true/false)
- `search`: Search in title and description
## Example Responses
**Success (200 OK):**
```json
{
"data": {
"id": 1,
"title": "Task title",
"completed": false
}
}
```
**Error (404 Not Found):**
```json
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "Task not found"
}
}
```
Best Practices Applied
Our API demonstrates these REST principles:
- ✅ Resource-based URLs (/tasks, not /getTasks)
- ✅ Proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
- ✅ Correct status codes (200, 201, 204, 404, 422, 500)
- ✅ JSON format for all requests/responses
- ✅ Consistent response structure (data + meta/error)
- ✅ Input validation with helpful error messages
- ✅ Filtering and searching capabilities
- ✅ API versioning (/api/v1/)
- ✅ ISO 8601 timestamps
- ✅ RESTful action endpoint (toggle)
Next Steps: Enhancing Your API
1. Add Pagination
app.get('/api/v1/tasks', (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const paginatedTasks = tasks.slice(startIndex, endIndex);
res.json({
data: paginatedTasks,
meta: {
current_page: page,
per_page: limit,
total: tasks.length,
total_pages: Math.ceil(tasks.length / limit)
}
});
});
2. Add Sorting
const sortBy = req.query.sort || 'createdAt';
const order = req.query.order === 'asc' ? 1 : -1;
tasks.sort((a, b) => {
if (a[sortBy] < b[sortBy]) return -1 * order;
if (a[sortBy] > b[sortBy]) return 1 * order;
return 0;
});
3. Add Authentication
// Middleware to check authentication
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required'
}
});
}
// Verify token here
next();
};
// Apply to protected routes
app.get('/api/v1/tasks', authenticate, (req, res) => {
// Route handler
});
4. Connect to a Database
Replace the in-memory array with MongoDB, PostgreSQL, or MySQL for persistent storage.
Add these features to your Task API:
- Add a "priority" field (low, medium, high) to tasks
- Create an endpoint to get task statistics (total, completed, pending)
- Add ability to assign tasks to users (assume user IDs)
- Implement due dates with validation (can't be in the past)
- Add endpoint to get overdue tasks
Key Takeaways
- REST APIs are built on standard HTTP methods and status codes
- Always validate input before processing
- Return appropriate status codes (201 for creation, 204 for deletion)
- Structure responses consistently (data/error wrappers)
- Provide helpful error messages with codes
- Version your API from the start (/api/v1/)
- Test thoroughly with tools like Postman or cURL
- Document your API endpoints and expected formats