Building a REST API with Node.js and Express for Beginners
Build your first REST API with Node.js and Express: routes, middleware, error handling, authentication, and connecting a database — for beginners.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Building a REST API with Node.js and Express for Beginners
My first API was a mess. I had all my code in one file, zero error handling, and hardcoded data. It worked for demos but would have fallen apart the moment two users tried to use it simultaneously.
Building APIs properly isn't complicated — it's a set of patterns. Once you know the patterns, every API you build follows the same structure and you never start from scratch again.
This tutorial builds a complete Task management API from scratch: creating tasks, reading them, updating, deleting, basic auth, and a real database. By the end, you'll have a production-ready structure you can use as a starter for any project.
Setup
mkdir task-api
cd task-api
npm init -y
npm install express cors dotenv
npm install -D nodemon
Create src/index.js:
const express = require('express');
const cors = require('cors');
const app = express();
// Middleware
app.use(cors());
app.use(express.json()); // Parse JSON request bodies
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Add to package.json:
{
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
}
}
npm run dev
Project Structure
src/
index.js → Entry point, server setup
routes/
tasks.js → Task routes
auth.js → Auth routes
controllers/
tasks.js → Task business logic
auth.js → Auth business logic
middleware/
auth.js → JWT verification
errorHandler.js → Centralized error handling
models/
task.js → Task data model
Building the Tasks API
Create the Route File
// src/routes/tasks.js
const express = require('express');
const router = express.Router();
// In-memory storage (we'll add a real DB later)
let tasks = [
{ id: 1, title: 'Learn Express', done: false, createdAt: new Date() },
{ id: 2, title: 'Build an API', done: false, createdAt: new Date() },
];
let nextId = 3;
// GET /api/tasks — get all tasks
router.get('/', (req, res) => {
res.json({ tasks, total: tasks.length });
});
// GET /api/tasks/:id — get single task
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(task);
});
// POST /api/tasks — create task
router.post('/', (req, res) => {
const { title } = req.body;
if (!title || typeof title !== 'string' || !title.trim()) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = {
id: nextId++,
title: title.trim(),
done: false,
createdAt: new Date(),
};
tasks.push(newTask);
res.status(201).json(newTask);
});
// PATCH /api/tasks/:id — update task
router.patch('/:id', (req, res) => {
const id = parseInt(req.params.id);
const taskIndex = tasks.findIndex(t => t.id === id);
if (taskIndex === -1) {
return res.status(404).json({ error: 'Task not found' });
}
const { title, done } = req.body;
const task = tasks[taskIndex];
if (title !== undefined) task.title = title.trim();
if (done !== undefined) task.done = Boolean(done);
task.updatedAt = new Date();
res.json(task);
});
// DELETE /api/tasks/:id — delete task
router.delete('/:id', (req, res) => {
const id = parseInt(req.params.id);
const initialLength = tasks.length;
tasks = tasks.filter(t => t.id !== id);
if (tasks.length === initialLength) {
return res.status(404).json({ error: 'Task not found' });
}
res.status(204).send(); // 204 No Content — success, nothing to return
});
module.exports = router;
Register the routes in index.js:
const tasksRouter = require('./routes/tasks');
app.use('/api/tasks', tasksRouter);
Test with curl:
# Get all tasks
curl http://localhost:3000/api/tasks
# Create a task
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "New Task"}'
# Update a task
curl -X PATCH http://localhost:3000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"done": true}'
# Delete a task
curl -X DELETE http://localhost:3000/api/tasks/1
Middleware: The Express Superpower
Middleware functions run between the request and response. They're how you add authentication, logging, validation, and more.
Logging Middleware
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
});
next(); // Must call next() to continue to the next middleware/route
}
app.use(requestLogger);
Centralized Error Handling
// src/middleware/errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
const status = err.status || 500;
const message = err.message || 'Internal server error';
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
}
module.exports = errorHandler;
// In index.js — AFTER all routes
const errorHandler = require('./middleware/errorHandler');
app.use(errorHandler);
Now in routes, you can throw errors or call next(error):
router.get('/:id', (req, res, next) => {
try {
const task = findTask(req.params.id);
if (!task) {
const error = new Error('Task not found');
error.status = 404;
throw error;
}
res.json(task);
} catch (err) {
next(err); // Forwards to errorHandler
}
});
Adding JWT Authentication
npm install jsonwebtoken bcrypt
// src/routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
// In-memory users (replace with database)
const users = [];
router.post('/register', async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const existing = users.find(u => u.email === email);
if (existing) {
return res.status(409).json({ error: 'Email already registered' });
}
const passwordHash = await bcrypt.hash(password, 10);
const user = { id: Date.now(), email, passwordHash };
users.push(user);
const token = jwt.sign({ userId: user.id, email }, JWT_SECRET, { expiresIn: '7d' });
res.status(201).json({ token });
} catch (err) {
next(err);
}
});
router.post('/login', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id, email }, JWT_SECRET, { expiresIn: '7d' });
res.json({ token });
} catch (err) {
next(err);
}
});
module.exports = router;
Auth Middleware
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization header required' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload; // Attach user to request
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
module.exports = requireAuth;
Apply to protected routes:
const requireAuth = require('./middleware/auth');
// Only authenticated users can create/update/delete
router.post('/', requireAuth, (req, res) => { ... });
router.patch('/:id', requireAuth, (req, res) => { ... });
router.delete('/:id', requireAuth, (req, res) => { ... });
What to Learn Next
This API uses in-memory storage. The natural next step is connecting to a real database. For JavaScript APIs, popular choices are:
- PostgreSQL with the
pgpackage or Prisma ORM - MongoDB with Mongoose
- SQLite for simple local databases
For calling this API from a React frontend, our React tutorial for beginners shows how to fetch and display data. When your frontend grows to need more structure, our Next.js 14 App Router guide shows how to build full-stack apps. And for understanding GraphQL as an alternative to REST, see our GraphQL vs REST guide.
Frequently Asked Questions
What is Express.js and why use it?
Express is a minimal Node.js web framework that adds routing, middleware, and HTTP helpers. It makes building APIs dramatically faster than raw Node.js.
What is a REST API?
An API that uses HTTP methods (GET/POST/PUT/DELETE) on resource URLs. Stateless — each request is independent. Returns JSON.
How do I handle errors in Express?
Create an error-handling middleware (4 params: err, req, res, next). Place it after all routes. Call next(error) in route handlers to forward to it.
How do I add authentication?
JWT: sign a token on login, verify it in middleware, attach user to req.user. Apply auth middleware to protected routes.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
How to Deploy a React App to Vercel in 10 Minutes
Deploy a React app to Vercel in 10 minutes: from npm create vite to live URL, custom domain setup, environment variables, and preview deployments.
GraphQL vs REST: Which API Style Should You Learn in 2025?
GraphQL vs REST API compared honestly for 2025: when each makes sense, real code examples, and which API style to learn first as a developer.
JavaScript Promises and Async/Await: Finally Understand Them
JavaScript async await and Promises explained clearly: the event loop, Promise chains, async/await patterns, error handling, and common mistakes to avoid.
How to Pass a JavaScript Interview at Google, Meta, or Amazon
How to pass a JavaScript interview at top tech companies: closures, event loop, promises, DOM questions, system design, and real interview questions answered.