Toolmingo
Guides23 min read

Express.js Tutorial for Beginners (2025): Learn Express Step by Step

Complete Express.js tutorial for absolute beginners. Learn Express from scratch: routing, middleware, REST APIs, templates, error handling, authentication, and build real projects. Free guide with code examples.

Express.js is the most popular Node.js web framework — minimal, flexible, and used by millions of developers to build web servers, REST APIs, and full-stack applications. If you know basic JavaScript and Node.js, Express is the fastest path to building real backends.

This tutorial takes you from zero to building a complete REST API with authentication, database integration, and production-ready patterns.

What you'll learn

Topic What you'll be able to do
Setup Install Express and run your first server
Routing Handle GET, POST, PUT, DELETE requests
Middleware Use and write custom middleware
REST APIs Build a complete CRUD API
Templates Render HTML with EJS
Error handling Handle errors gracefully
Authentication Protect routes with JWT
Database Connect to MongoDB and PostgreSQL
Real projects 3 complete projects

Why Express.js?

Express.js Fastify Koa NestJS Hono
Speed Fast Fastest Fast Fast Very fast
Learning curve Very low Low Low High Low
Ecosystem Massive Growing Smaller Large Growing
Opinionated No No No Yes No
TypeScript Optional Optional Optional First-class Optional
Best for General, APIs, prototyping High-perf APIs Middleware-heavy Enterprise, structure Edge, microservices

Express dominates because:

  • ~60 million npm downloads/week — largest backend Node.js framework
  • Minimal core — add only what you need
  • Huge ecosystem — thousands of compatible middleware packages
  • Job market — required in most Node.js/backend job descriptions

Prerequisites

You need:

  • Basic JavaScript (variables, functions, objects, arrays, Promises)
  • Node.js installed (v18+ recommended — nodejs.org)
  • A terminal and code editor (VS Code recommended)

Verify Node.js is installed:

node --version   # v18.x.x or higher
npm --version    # 9.x.x or higher

1. Your First Express Server

Installation

mkdir my-express-app
cd my-express-app
npm init -y
npm install express

Hello World

Create index.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello, Express!');
});

app.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

Run it:

node index.js

Open http://localhost:3000 — you'll see Hello, Express!

Using nodemon for development

Install nodemon to auto-restart on file changes:

npm install --save-dev nodemon

Add to package.json:

{
  "scripts": {
    "dev": "nodemon index.js",
    "start": "node index.js"
  }
}

Now run npm run dev and the server restarts automatically.


2. Routing

Routes map HTTP methods and URL paths to handler functions.

Basic routes

const express = require('express');
const app = express();

// GET request
app.get('/', (req, res) => {
  res.send('Home page');
});

// POST request
app.post('/users', (req, res) => {
  res.send('Create user');
});

// PUT request
app.put('/users/:id', (req, res) => {
  res.send(`Update user ${req.params.id}`);
});

// DELETE request
app.delete('/users/:id', (req, res) => {
  res.send(`Delete user ${req.params.id}`);
});

app.listen(3000);

Route parameters

// URL: /users/42
app.get('/users/:id', (req, res) => {
  const { id } = req.params;
  res.json({ userId: id });
});

// Multiple params: /posts/2025/javascript
app.get('/posts/:year/:slug', (req, res) => {
  const { year, slug } = req.params;
  res.json({ year, slug });
});

Query strings

// URL: /search?q=express&page=2
app.get('/search', (req, res) => {
  const { q, page = 1 } = req.query;
  res.json({ query: q, page: Number(page) });
});

express.Router

Organize routes into separate files:

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => res.json({ users: [] }));
router.post('/', (req, res) => res.json({ message: 'User created' }));
router.get('/:id', (req, res) => res.json({ id: req.params.id }));
router.put('/:id', (req, res) => res.json({ message: 'User updated' }));
router.delete('/:id', (req, res) => res.json({ message: 'User deleted' }));

module.exports = router;
// index.js
const express = require('express');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());

app.use('/api/users', usersRouter);

app.listen(3000);

HTTP methods cheat sheet

Method Purpose Body Idempotent
GET Read resource No Yes
POST Create resource Yes No
PUT Replace resource Yes Yes
PATCH Partial update Yes Yes
DELETE Remove resource No Yes

3. Middleware

Middleware are functions that run between the request and response. They have access to req, res, and next.

Request → Middleware 1 → Middleware 2 → Route Handler → Response

Built-in middleware

const express = require('express');
const app = express();

// Parse JSON body
app.use(express.json());

// Parse URL-encoded forms
app.use(express.urlencoded({ extended: true }));

// Serve static files from 'public' directory
app.use(express.static('public'));

Writing custom middleware

// Logging middleware
const logger = (req, res, next) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  next(); // MUST call next() to continue
};

// Apply globally
app.use(logger);

// Apply to specific route
app.get('/protected', logger, (req, res) => {
  res.send('Protected route');
});

Middleware execution order

app.use((req, res, next) => {
  console.log('Middleware 1');
  next();
});

app.use((req, res, next) => {
  console.log('Middleware 2');
  next();
});

app.get('/', (req, res) => {
  console.log('Route handler');
  res.send('Done');
});
// Output: Middleware 1 → Middleware 2 → Route handler

Popular middleware packages

Package Purpose Install
cors Enable CORS npm i cors
helmet Security headers npm i helmet
morgan HTTP request logging npm i morgan
compression Gzip responses npm i compression
express-rate-limit Rate limiting npm i express-rate-limit
cookie-parser Parse cookies npm i cookie-parser
multer File uploads npm i multer
express-validator Input validation npm i express-validator

Using third-party middleware:

const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

app.use(cors());
app.use(helmet());
app.use(morgan('dev'));

4. Request and Response Objects

Request object (req)

app.post('/example', (req, res) => {
  req.params      // URL params: { id: '42' }
  req.query       // Query string: { page: '2' }
  req.body        // Request body (needs express.json())
  req.headers     // HTTP headers
  req.method      // 'GET', 'POST', etc.
  req.url         // '/example'
  req.ip          // Client IP
  req.cookies     // Cookies (needs cookie-parser)
  req.get('Authorization')  // Get specific header
});

Response object (res)

app.get('/demo', (req, res) => {
  // Send text
  res.send('Hello');

  // Send JSON
  res.json({ message: 'ok' });

  // Set status code
  res.status(201).json({ created: true });

  // Send HTML
  res.send('<h1>Hello</h1>');

  // Redirect
  res.redirect('/new-url');
  res.redirect(301, '/permanent-url');

  // Set header
  res.set('Content-Type', 'text/plain');
  res.set('X-Custom-Header', 'value');

  // Set cookie
  res.cookie('session', 'abc123', { httpOnly: true, secure: true });

  // Clear cookie
  res.clearCookie('session');

  // Send file
  res.sendFile('/absolute/path/to/file.html');

  // Download file
  res.download('/path/to/file.pdf', 'report.pdf');
});

Common status codes

Code Meaning When to use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST
204 No Content Successful DELETE
400 Bad Request Invalid input
401 Unauthorized Not logged in
403 Forbidden Logged in but no permission
404 Not Found Resource doesn't exist
409 Conflict Duplicate resource
422 Unprocessable Entity Validation error
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server error

5. Handling Request Body

app.use(express.json()); // Required for JSON bodies

app.post('/users', (req, res) => {
  const { name, email, age } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  // Process the data...
  res.status(201).json({ message: 'User created', user: { name, email, age } });
});

Test with curl:

curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com", "age": 30}'

Input validation with express-validator

npm install express-validator
const { body, validationResult } = require('express-validator');

app.post('/users',
  // Validation rules
  body('email').isEmail().normalizeEmail(),
  body('name').notEmpty().trim().isLength({ min: 2, max: 50 }),
  body('age').optional().isInt({ min: 0, max: 120 }),

  // Handler
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    const { name, email, age } = req.body;
    res.status(201).json({ message: 'User created', user: { name, email, age } });
  }
);

6. Error Handling

Synchronous errors

app.get('/error', (req, res, next) => {
  try {
    throw new Error('Something went wrong');
  } catch (err) {
    next(err); // Pass to error handler
  }
});

Async errors (Express 5 or manual)

// Express 4: wrap async handlers
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await getUserById(req.params.id);
  if (!user) {
    const err = new Error('User not found');
    err.status = 404;
    throw err;
  }
  res.json(user);
}));

Global error handler

// Must have 4 parameters: (err, req, res, next)
app.use((err, req, res, next) => {
  const status = err.status || 500;
  const message = err.message || 'Internal Server Error';

  console.error(`[${new Date().toISOString()}] ${status} - ${message}`);

  res.status(status).json({
    error: {
      message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
    },
  });
});

404 handler (place before error handler)

app.use((req, res, next) => {
  res.status(404).json({ error: `Route ${req.method} ${req.url} not found` });
});

Custom error class

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.status = statusCode;
    this.name = 'AppError';
  }
}

// Usage
app.get('/items/:id', asyncHandler(async (req, res) => {
  const item = await findItem(req.params.id);
  if (!item) throw new AppError('Item not found', 404);
  res.json(item);
}));

7. REST API — Complete CRUD Example

A full in-memory notes API:

const express = require('express');
const app = express();

app.use(express.json());

// In-memory store
let notes = [
  { id: 1, title: 'First note', body: 'Hello world', createdAt: new Date().toISOString() },
];
let nextId = 2;

// GET all notes
app.get('/api/notes', (req, res) => {
  const { search } = req.query;
  let result = notes;

  if (search) {
    const term = search.toLowerCase();
    result = notes.filter(n =>
      n.title.toLowerCase().includes(term) ||
      n.body.toLowerCase().includes(term)
    );
  }

  res.json({ notes: result, total: result.length });
});

// GET single note
app.get('/api/notes/:id', (req, res) => {
  const id = Number(req.params.id);
  const note = notes.find(n => n.id === id);
  if (!note) return res.status(404).json({ error: 'Note not found' });
  res.json(note);
});

// POST create note
app.post('/api/notes', (req, res) => {
  const { title, body } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

  const note = { id: nextId++, title, body: body || '', createdAt: new Date().toISOString() };
  notes.push(note);
  res.status(201).json(note);
});

// PUT update note
app.put('/api/notes/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = notes.findIndex(n => n.id === id);
  if (index === -1) return res.status(404).json({ error: 'Note not found' });

  const { title, body } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });

  notes[index] = { ...notes[index], title, body: body || '', updatedAt: new Date().toISOString() };
  res.json(notes[index]);
});

// PATCH partial update
app.patch('/api/notes/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = notes.findIndex(n => n.id === id);
  if (index === -1) return res.status(404).json({ error: 'Note not found' });

  const updates = {};
  if (req.body.title !== undefined) updates.title = req.body.title;
  if (req.body.body !== undefined) updates.body = req.body.body;

  notes[index] = { ...notes[index], ...updates, updatedAt: new Date().toISOString() };
  res.json(notes[index]);
});

// DELETE note
app.delete('/api/notes/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = notes.findIndex(n => n.id === id);
  if (index === -1) return res.status(404).json({ error: 'Note not found' });

  notes.splice(index, 1);
  res.status(204).send();
});

// Error handler
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message || 'Server error' });
});

app.listen(3000, () => console.log('Notes API at http://localhost:3000'));

Test the API:

# Get all notes
curl http://localhost:3000/api/notes

# Create note
curl -X POST http://localhost:3000/api/notes \
  -H "Content-Type: application/json" \
  -d '{"title": "My note", "body": "Note content"}'

# Update note
curl -X PUT http://localhost:3000/api/notes/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated", "body": "New content"}'

# Delete note
curl -X DELETE http://localhost:3000/api/notes/1

# Search
curl "http://localhost:3000/api/notes?search=hello"

8. Serving Templates with EJS

For server-rendered HTML:

npm install ejs
const express = require('express');
const path = require('path');
const app = express();

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));

const todos = [
  { id: 1, text: 'Learn Express', done: true },
  { id: 2, text: 'Build an API', done: false },
];

app.get('/', (req, res) => {
  res.render('index', {
    title: 'My Todo App',
    todos,
    count: todos.filter(t => !t.done).length,
  });
});

app.listen(3000);

views/index.ejs:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><%= title %></title>
</head>
<body>
  <h1><%= title %></h1>
  <p><%= count %> task(s) remaining</p>
  <ul>
    <% todos.forEach(todo => { %>
      <li style="<%= todo.done ? 'text-decoration: line-through' : '' %>">
        <%= todo.text %>
      </li>
    <% }) %>
  </ul>
</body>
</html>

EJS syntax

Syntax Purpose Example
<%= value %> Output escaped HTML <%= user.name %>
<%- value %> Output raw HTML <%- htmlContent %>
<% code %> Execute JS (no output) <% if (user) { %>
<%# comment %> EJS comment <%# this is hidden %>
<%- include('partial') %> Include partial <%- include('header') %>

9. Authentication with JWT

npm install jsonwebtoken bcryptjs
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-prod';

// Mock user store (use a real database in production)
const users = [];

// Register
app.post('/api/auth/register', async (req, res) => {
  const { email, password, name } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: 'Email and password required' });
  }

  if (users.find(u => u.email === email)) {
    return res.status(409).json({ error: 'Email already registered' });
  }

  const hashedPassword = await bcrypt.hash(password, 10);
  const user = { id: users.length + 1, email, name, password: hashedPassword };
  users.push(user);

  const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
    expiresIn: '7d',
  });

  res.status(201).json({ token, user: { id: user.id, email, name } });
});

// Login
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  const user = users.find(u => u.email === email);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const valid = await bcrypt.compare(password, user.password);
  if (!valid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
    expiresIn: '7d',
  });

  res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
});

// Auth middleware
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

// Protected route
app.get('/api/me', authenticate, (req, res) => {
  const user = users.find(u => u.id === req.user.userId);
  if (!user) return res.status(404).json({ error: 'User not found' });

  const { password, ...safeUser } = user;
  res.json(safeUser);
});

app.listen(3000);

Test:

# Register
curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "password": "secret123", "name": "Alice"}'

# Login
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "password": "secret123"}'

# Access protected route (replace TOKEN with the token from login)
curl http://localhost:3000/api/me \
  -H "Authorization: Bearer TOKEN"

10. Connecting to a Database

MongoDB with Mongoose

npm install mongoose
const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp')
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error('MongoDB error:', err));

// Define schema
const postSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true },
  body: { type: String, required: true },
  author: { type: String, default: 'Anonymous' },
  published: { type: Boolean, default: false },
}, { timestamps: true });

const Post = mongoose.model('Post', postSchema);

// Routes
app.get('/api/posts', async (req, res) => {
  const posts = await Post.find({ published: true }).sort({ createdAt: -1 });
  res.json(posts);
});

app.post('/api/posts', async (req, res) => {
  try {
    const post = new Post(req.body);
    await post.save();
    res.status(201).json(post);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.get('/api/posts/:id', async (req, res) => {
  try {
    const post = await Post.findById(req.params.id);
    if (!post) return res.status(404).json({ error: 'Post not found' });
    res.json(post);
  } catch {
    res.status(400).json({ error: 'Invalid ID' });
  }
});

app.delete('/api/posts/:id', async (req, res) => {
  const post = await Post.findByIdAndDelete(req.params.id);
  if (!post) return res.status(404).json({ error: 'Post not found' });
  res.status(204).send();
});

app.listen(3000);

PostgreSQL with pg

npm install pg
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgresql://localhost/myapp',
});

// Create table (run once)
pool.query(`
  CREATE TABLE IF NOT EXISTS todos (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    done BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW()
  )
`);

// GET all todos
app.get('/api/todos', async (req, res) => {
  const result = await pool.query('SELECT * FROM todos ORDER BY created_at DESC');
  res.json(result.rows);
});

// POST create todo
app.post('/api/todos', async (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'Title required' });

  const result = await pool.query(
    'INSERT INTO todos (title) VALUES ($1) RETURNING *',
    [title]
  );
  res.status(201).json(result.rows[0]);
});

// PATCH toggle done
app.patch('/api/todos/:id', async (req, res) => {
  const result = await pool.query(
    'UPDATE todos SET done = NOT done WHERE id = $1 RETURNING *',
    [req.params.id]
  );
  if (result.rowCount === 0) return res.status(404).json({ error: 'Not found' });
  res.json(result.rows[0]);
});

11. Project Structure

A well-organized Express project:

my-api/
├── src/
│   ├── routes/
│   │   ├── users.js
│   │   ├── posts.js
│   │   └── index.js          # Combine all routes
│   ├── middleware/
│   │   ├── auth.js
│   │   ├── errorHandler.js
│   │   └── validate.js
│   ├── models/
│   │   ├── User.js
│   │   └── Post.js
│   ├── controllers/
│   │   ├── userController.js
│   │   └── postController.js
│   ├── services/
│   │   └── emailService.js
│   ├── config/
│   │   └── database.js
│   └── app.js                # Express setup (no listen)
├── index.js                  # Entry point (listen here)
├── .env
├── .env.example
├── .gitignore
└── package.json

src/app.js:

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// Middleware
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api', routes);

// 404
app.use((req, res) => {
  res.status(404).json({ error: `${req.method} ${req.url} not found` });
});

// Error handler (must be last)
app.use(errorHandler);

module.exports = app;

index.js:

const app = require('./src/app');

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

12. Environment Variables

Never hardcode secrets. Use .env files:

npm install dotenv

.env:

PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://localhost/mydb
JWT_SECRET=your-super-secret-key-change-this
CORS_ORIGIN=http://localhost:5173

.gitignore:

node_modules/
.env
*.log

Load in index.js (first line):

require('dotenv').config();
const app = require('./src/app');

const PORT = process.env.PORT || 3000;
app.listen(PORT);

Access anywhere:

const secret = process.env.JWT_SECRET;
const dbUrl = process.env.DATABASE_URL;

3 Projects

Project 1: Todo REST API

A simple but complete REST API with filtering and persistence:

// todo-api/index.js
require('dotenv').config();
const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(express.json());

const DATA_FILE = path.join(__dirname, 'todos.json');

const load = () => {
  if (!fs.existsSync(DATA_FILE)) return [];
  return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
};

const save = (todos) => {
  fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2));
};

let todos = load();
let nextId = todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1;

// GET all (with optional filter)
app.get('/api/todos', (req, res) => {
  const { done, search } = req.query;
  let result = todos;

  if (done !== undefined) {
    result = result.filter(t => t.done === (done === 'true'));
  }
  if (search) {
    const term = search.toLowerCase();
    result = result.filter(t => t.title.toLowerCase().includes(term));
  }

  res.json({
    todos: result,
    total: result.length,
    pending: todos.filter(t => !t.done).length,
  });
});

// POST create
app.post('/api/todos', (req, res) => {
  const { title, priority = 'medium' } = req.body;
  if (!title?.trim()) {
    return res.status(400).json({ error: 'Title required' });
  }

  const todo = {
    id: nextId++,
    title: title.trim(),
    done: false,
    priority,
    createdAt: new Date().toISOString(),
  };
  todos.push(todo);
  save(todos);
  res.status(201).json(todo);
});

// PATCH toggle/update
app.patch('/api/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Not found' });

  if (req.body.done !== undefined) todo.done = req.body.done;
  if (req.body.title) todo.title = req.body.title;
  todo.updatedAt = new Date().toISOString();
  save(todos);
  res.json(todo);
});

// DELETE
app.delete('/api/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = todos.findIndex(t => t.id === id);
  if (index === -1) return res.status(404).json({ error: 'Not found' });

  todos.splice(index, 1);
  save(todos);
  res.status(204).send();
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Todo API on http://localhost:${PORT}`));

Project 2: Blog API with Authentication

Full CRUD blog API with JWT auth and user ownership:

// blog-api/index.js
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
app.use(express.json());

const SECRET = process.env.JWT_SECRET || 'dev-secret';

// In-memory stores
const users = [];
const posts = [];
let userId = 1;
let postId = 1;

// Auth middleware
const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Auth routes
app.post('/api/register', async (req, res) => {
  const { email, password, name } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
  if (users.find(u => u.email === email)) return res.status(409).json({ error: 'Email taken' });

  const user = { id: userId++, email, name, password: await bcrypt.hash(password, 10) };
  users.push(user);

  const token = jwt.sign({ id: user.id, email }, SECRET, { expiresIn: '7d' });
  res.status(201).json({ token, user: { id: user.id, email, name } });
});

app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  const user = users.find(u => u.email === email);
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const token = jwt.sign({ id: user.id, email }, SECRET, { expiresIn: '7d' });
  res.json({ token, user: { id: user.id, email, name: user.name } });
});

// Post routes
app.get('/api/posts', (req, res) => {
  res.json(posts.map(p => ({ ...p, body: p.body.slice(0, 200) + '...' })));
});

app.get('/api/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === Number(req.params.id));
  if (!post) return res.status(404).json({ error: 'Not found' });
  res.json(post);
});

app.post('/api/posts', auth, (req, res) => {
  const { title, body } = req.body;
  if (!title || !body) return res.status(400).json({ error: 'Title and body required' });

  const post = { id: postId++, title, body, authorId: req.user.id, createdAt: new Date().toISOString() };
  posts.push(post);
  res.status(201).json(post);
});

app.put('/api/posts/:id', auth, (req, res) => {
  const post = posts.find(p => p.id === Number(req.params.id));
  if (!post) return res.status(404).json({ error: 'Not found' });
  if (post.authorId !== req.user.id) return res.status(403).json({ error: 'Forbidden' });

  const { title, body } = req.body;
  if (title) post.title = title;
  if (body) post.body = body;
  post.updatedAt = new Date().toISOString();
  res.json(post);
});

app.delete('/api/posts/:id', auth, (req, res) => {
  const index = posts.findIndex(p => p.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Not found' });
  if (posts[index].authorId !== req.user.id) return res.status(403).json({ error: 'Forbidden' });

  posts.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => console.log('Blog API on http://localhost:3000'));

Project 3: File Upload API

Handle image uploads with multer:

npm install multer sharp
// upload-api/index.js
const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');

const app = express();
const UPLOAD_DIR = path.join(__dirname, 'uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true });

// Store files in memory, process with sharp
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Only JPEG, PNG, and WebP are allowed'));
    }
  },
});

app.post('/api/upload', upload.single('image'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file uploaded' });

  const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.webp`;
  const outputPath = path.join(UPLOAD_DIR, filename);

  await sharp(req.file.buffer)
    .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 85 })
    .toFile(outputPath);

  const stats = fs.statSync(outputPath);
  res.json({
    filename,
    url: `/uploads/${filename}`,
    originalSize: req.file.size,
    processedSize: stats.size,
    savedPercent: Math.round((1 - stats.size / req.file.size) * 100),
  });
});

// Serve uploaded files
app.use('/uploads', express.static(UPLOAD_DIR));

// Handle multer errors
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large (max 5MB)' });
    }
  }
  res.status(400).json({ error: err.message });
});

app.listen(3000, () => console.log('Upload API on http://localhost:3000'));

Test:

curl -X POST http://localhost:3000/api/upload \
  -F "image=@/path/to/photo.jpg"

Express vs Related Terms

Term What it is
Express Minimal Node.js web framework
Koa Next-gen framework from Express team, uses async/await natively
Fastify Faster Express alternative, schema-based validation
NestJS Opinionated framework built on Express, Angular-like structure
Hono Ultra-fast, edge-compatible framework
middleware Function that processes req/res before route handler
router Express module for organizing routes in groups
EJS Embedded JavaScript templating engine
Pug Another Express-compatible template engine (formerly Jade)
multer Middleware for handling multipart/form-data (file uploads)
cors Cross-Origin Resource Sharing middleware
helmet Security middleware that sets HTTP headers

Common Mistakes

Mistake Problem Fix
Forgetting express.json() req.body is undefined Add app.use(express.json()) before routes
No error handler with 4 params Errors not caught globally Use (err, req, res, next) signature
Not calling next(err) in async Unhandled promise rejections Wrap async handlers or use asyncHandler
Sending response twice Cannot set headers after they are sent Add return before res.send() / res.json()
Committing .env Secrets exposed Add .env to .gitignore
app.use() after routes Middleware never runs Register middleware before route handlers
No 404 handler Requests to unknown routes hang or return odd responses Add app.use((req, res) => res.status(404).json(...))
Hardcoded secrets Security vulnerability Use process.env.SECRET + .env

Learning Path

Stage Topics Resources
1. Basics Routing, req/res, static files This guide + Express docs
2. Middleware Custom middleware, error handling, popular packages Express docs
3. REST APIs CRUD, status codes, JSON, validation Build the projects above
4. Auth JWT, sessions, OAuth passport.js, jsonwebtoken docs
5. Databases MongoDB/Mongoose or PostgreSQL/pg or Prisma Respective docs
6. Testing Jest + Supertest for API testing Supertest docs
7. Production Rate limiting, logging, env config, Docker Morgan, helmet, docker docs

Express vs Other Node.js Frameworks

Express Fastify Koa NestJS Hono
Weekly downloads ~60M ~8M ~3M ~5M ~2M
Bundle size Small Small Small Large Tiny
Performance Good Best Good Good Excellent
Learning curve Very Low Low Low High Low
TypeScript Optional Optional Optional First-class Optional
Structure Unopinionated Unopinionated Unopinionated MVC / DI Unopinionated
Middleware Custom API Plugin API async functions Guards/Interceptors async functions
Best for General APIs, learning, prototyping High-perf APIs Minimal APIs Enterprise apps Edge functions

6 FAQ

Q: Do I need to know Node.js before learning Express?
Basic Node.js — modules, npm, require, basic async — is enough to start. You'll deepen Node.js knowledge while learning Express.

Q: Should I use Express or NestJS for a new project?
Express for prototypes, simple APIs, or when you want full control. NestJS if you're building a large-scale application and want Angular-like structure with decorators and dependency injection.

Q: How do I handle file uploads?
Use the multer middleware. See Project 3 above. For large-scale file handling, upload to cloud storage (S3, Cloudinary) instead of local disk.

Q: Is Express still relevant in 2025?
Yes — Express has ~60M weekly downloads and is used in production by countless companies. Newer frameworks are faster, but Express's ecosystem, documentation, and job-market presence remain dominant. Express 5 (with native async error handling) was also released.

Q: How do I deploy an Express app?
Common options: Render, Railway, Heroku (paid), Fly.io, or a VPS with PM2 + Nginx. Docker works on any platform. Run node index.js in production — or use a process manager like PM2 for auto-restart.

Q: REST API vs GraphQL — which should I build with Express?
REST for most APIs — simpler, better cache support, widely understood. GraphQL when clients need to fetch exactly the data shape they want (common in large frontends with many data types). You can use Express with either via express-graphql or @apollo/server.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools