REST API Development
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.
Note: This lesson uses Node.js with Express framework for examples, but the concepts apply to any language (PHP, Python, Java, etc.). Focus on understanding the REST principles rather than memorizing syntax.
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
Tip: Using nodemon in development automatically restarts the server when you save changes. This speeds up development significantly.
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
});
});
Important: Notice we return 201 (Created) status code for successful creation, not 200. We also validate the input before creating the resource.
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 })
}
});
});
Warning: Never expose detailed error messages or stack traces in production. They can reveal sensitive information about your system architecture.
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"
}
}
```
Tip: Consider using tools like Swagger/OpenAPI to generate interactive API documentation automatically from your code.
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.
Exercise: Extend the API
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
Congratulations! You've built your first REST API from scratch! You now understand how to design endpoints, handle requests, validate data, and return proper responses. The next step is to practice building more complex APIs and exploring advanced topics like authentication, rate limiting, and API security.