Create a backend API for managing tasks. You will practice Express routing, HTTP methods, JSON responses, and REST conventions.

Requirements

Features

  • CRUD endpoints for a tasks resource
  • In-memory data store (no database required)
  • Input validation and proper HTTP status codes
  • JSON request/response bodies

Step 1: Initialize the Project

  mkdir task-api && cd task-api
npm init -y
npm install express
  

Add to package.json:

  "type": "module"
  

Step 2: Basic Server

server.js

  import express from 'express';

const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
  

Run with node server.js and test: curl http://localhost:3000/health

Step 3: In-Memory Data Store

  let tasks = [
  { id: 1, title: 'Learn Express', completed: false },
  { id: 2, title: 'Build REST API', completed: false }
];
let nextId = 3;
  

Step 4: CRUD Endpoints

  // List all tasks
app.get('/api/tasks', (req, res) => {
  res.json(tasks);
});

// Get one task
app.get('/api/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) return res.status(404).json({ error: 'Task not found' });
  res.json(task);
});

// Create task
app.post('/api/tasks', (req, res) => {
  const { title } = req.body;
  if (!title || typeof title !== 'string') {
    return res.status(400).json({ error: 'Title is required' });
  }
  const task = { id: nextId++, title: title.trim(), completed: false };
  tasks.push(task);
  res.status(201).json(task);
});

// Update task
app.put('/api/tasks/:id', (req, res) => {
  const index = tasks.findIndex(t => t.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Task not found' });

  const { title, completed } = req.body;
  if (title !== undefined) tasks[index].title = title.trim();
  if (completed !== undefined) tasks[index].completed = Boolean(completed);
  res.json(tasks[index]);
});

// Delete task
app.delete('/api/tasks/:id', (req, res) => {
  const index = tasks.findIndex(t => t.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Task not found' });
  tasks.splice(index, 1);
  res.status(204).send();
});
  

Step 5: Test with curl

  curl http://localhost:3000/api/tasks
curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title": "Write tests"}'
curl -X PUT http://localhost:3000/api/tasks/1 -H "Content-Type: application/json" -d '{"completed": true}'
curl -X DELETE http://localhost:3000/api/tasks/1
  

Step 6: Add Middleware

Add logging, a 404 catch-all, and an error handler after your routes:

  app.use((req, res, next) => { console.log(`${req.method} ${req.url}`); next(); });
app.use((req, res) => res.status(404).json({ error: 'Route not found' }));
app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: 'Internal server error' }); });
  

Extension Ideas

  • Query filters — support ?completed=true on the list endpoint
  • Persistent storage — replace the array with SQLite or MongoDB
  • Pagination?page=1&limit=10 query parameters
  • Authentication — protect write endpoints with JWT middleware
  • Input sanitization — use a validation library like zod or joi
  • API documentation — generate OpenAPI/Swagger docs with swagger-jsdoc
  • Automated tests — write integration tests with supertest and vitest