DevToolBox無料
ブログ

Express.jsガイド:ルーティング、ミドルウェア、REST API、認証

13分by DevToolBox

Express.js Complete Guide: Build REST APIs with Node.js in 2026

Master Express.js from scratch with this comprehensive tutorial. Learn routing, middleware, REST API design, JWT authentication, error handling, and how Express compares to Fastify, Koa, and Hapi in 2026.

TL;DR — Express.js Quick Reference

Express.js is a minimal Node.js web framework for building REST APIs and web apps. Install with npm install express, define routes with app.get/post/put/delete, use middleware with app.use(), handle errors with a 4-argument middleware, and secure APIs with JWT + bcrypt. Express remains the most popular Node.js framework in 2026 with 30M+ weekly downloads.

What Is Express.js?

Express.js is a fast, unopinionated, minimalist web framework for Node.js. Released in 2010 by TJ Holowaychuk and now maintained by the OpenJS Foundation, Express has become the de-facto standard for building HTTP servers and REST APIs in the Node.js ecosystem.

The framework's philosophy is minimal by design: it provides only the essential tools — routing, middleware support, and HTTP request/response utilities — without imposing any particular project structure, template engine, or database layer. This flexibility is both Express's greatest strength and the reason it remains relevant in 2026 despite competition from newer frameworks.

As of 2026, Express receives over 30 million weekly downloads from npm, making it consistently one of the top 5 most downloaded packages in the entire JavaScript ecosystem. Over 30 million open-source projects list it as a dependency.

Key Takeaways
  • Express.js is a thin layer on top of Node.js's http module with routing and middleware.
  • Middleware functions are the building blocks — they process requests sequentially via next().
  • Use express.Router() to modularize routes and keep code organized.
  • Always add a centralized error-handling middleware (4 arguments) as the last app.use() call.
  • Secure production apps with helmet, cors, express-rate-limit, and JWT-based authentication.
  • Fastify is 2–3x faster than Express but Express wins on ecosystem size and community knowledge.
  • Express 5 (release candidate in 2026) adds native async/await error propagation.

Getting Started: Installation and First Server

Express.js requires Node.js v18 or higher (LTS recommended). Install it with npm or your preferred package manager, then create your first HTTP server in under 10 lines.

# Initialize a new Node.js project
mkdir my-api && cd my-api
npm init -y

# Install Express
npm install express

# Install TypeScript types (optional but recommended)
npm install --save-dev @types/express typescript ts-node

# Create the entry file
touch index.js

The simplest possible Express server responds "Hello World" to all GET requests on port 3000:

// index.js — Minimal Express server
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Parse JSON request bodies
app.use(express.json());

// Basic route
app.get('/', (req, res) => {
  res.json({ message: 'Hello World from Express!' });
});

// Start the server
app.listen(PORT, () => {
  console.log('Server running on http://localhost:' + PORT);
});
# Run it
node index.js
# Server running on http://localhost:3000

# Test with curl
curl http://localhost:3000
# {"message":"Hello World from Express!"}

Using TypeScript with Express

TypeScript provides type safety and better IDE support. Here is the same server with full TypeScript:

// src/index.ts — Express with TypeScript
import express, { Request, Response, NextFunction } from 'express';

const app = express();
const PORT: number = parseInt(process.env.PORT || '3000', 10);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req: Request, res: Response) => {
  res.json({ message: 'Hello from Express + TypeScript!', timestamp: new Date().toISOString() });
});

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

export default app;

Routing: GET, POST, PUT, DELETE

Express routing maps HTTP methods and URL paths to handler functions. Every handler receives a Request object, a Response object, and an optional next function.

Basic Route Methods

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

// GET — retrieve resources
app.get('/users', (req, res) => {
  res.json({ users: [] });
});

// POST — create a resource
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  // Create user in database...
  res.status(201).json({ id: 1, name, email });
});

// PUT — replace a resource entirely
app.put('/users/:id', (req, res) => {
  const { id } = req.params;
  const { name, email } = req.body;
  // Update user in database...
  res.json({ id, name, email });
});

// PATCH — partial update
app.patch('/users/:id', (req, res) => {
  const { id } = req.params;
  // Apply partial update...
  res.json({ id, ...req.body });
});

// DELETE — remove a resource
app.delete('/users/:id', (req, res) => {
  const { id } = req.params;
  // Delete user from database...
  res.status(204).send(); // No content
});

Route Parameters and Query Strings

// Route parameters — accessed via req.params
// URL: /users/42/posts/7
app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
  // { userId: '42', postId: '7' }
});

// Query strings — accessed via req.query
// URL: /search?q=express&page=2&limit=10
app.get('/search', (req, res) => {
  const { q, page = '1', limit = '20' } = req.query;
  res.json({
    query: q,
    page: parseInt(page as string, 10),
    limit: parseInt(limit as string, 10),
  });
});

// Optional parameters with regex
app.get('/files/:filename(*)' , (req, res) => {
  res.json({ file: req.params.filename });
});

Express Router — Modular Routes

For larger applications, use express.Router() to split routes into separate files. Each router is a mini Express application that handles its own middleware and routes.

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

// All routes here are relative to the mount path
router.get('/', (req, res) => {
  res.json({ users: [] }); // GET /api/users
});

router.post('/', (req, res) => {
  res.status(201).json({ created: true }); // POST /api/users
});

router.get('/:id', (req, res) => {
  res.json({ userId: req.params.id }); // GET /api/users/42
});

router.put('/:id', (req, res) => {
  res.json({ updated: req.params.id }); // PUT /api/users/42
});

router.delete('/:id', (req, res) => {
  res.status(204).send(); // DELETE /api/users/42
});

module.exports = router;

// src/app.js — mount the router
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');

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

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

module.exports = app;

Middleware: The Core of Express

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function (next). They form a pipeline: each middleware either responds to the request or calls next() to pass control forward.

This pipeline is what makes Express powerful — you can compose complex request processing from small, reusable pieces.

Built-in Middleware

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

// Parse JSON bodies (Content-Type: application/json)
app.use(express.json({ limit: '10mb' }));

// Parse URL-encoded bodies (HTML form submissions)
app.use(express.urlencoded({ extended: true }));

// Serve static files from the 'public' directory
app.use(express.static('public'));
// Serve at a specific URL prefix:
app.use('/assets', express.static('public'));

Third-Party Middleware

The npm ecosystem provides hundreds of middleware packages. These are the most essential for production:

npm install helmet cors morgan compression express-rate-limit
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

const app = express();

// Security headers (Content-Security-Policy, X-Frame-Options, etc.)
app.use(helmet());

// CORS — Allow requests from specific origins
app.use(cors({
  origin: ['https://myapp.com', 'https://www.myapp.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
}));

// HTTP request logging
app.use(morgan('combined')); // Apache-style log format
// or use 'dev' for concise colored output in development

// Gzip response compression
app.use(compression());

// Rate limiting — 100 requests per 15 minutes per IP
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: { error: 'Too many requests, please try again later.' },
});
app.use('/api/', limiter);

app.use(express.json());

Custom Middleware

Writing custom middleware is straightforward — it is just a function. Use it for authentication, logging, request transformation, or any cross-cutting concern.

// Request timing middleware
function requestTimer(req, res, next) {
  req.startTime = Date.now();

  // Override res.json to inject timing data
  const originalJson = res.json.bind(res);
  res.json = (body) => {
    const duration = Date.now() - req.startTime;
    res.set('X-Response-Time', duration + 'ms');
    return originalJson(body);
  };

  next();
}

// Request ID middleware
const { v4: uuidv4 } = require('uuid');
function requestId(req, res, next) {
  req.id = uuidv4();
  res.set('X-Request-Id', req.id);
  next();
}

// API key validation middleware
function validateApiKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  if (!apiKey || apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Invalid or missing API key' });
  }
  next();
}

// Route-level middleware — only applies to this route
app.get('/protected', validateApiKey, (req, res) => {
  res.json({ data: 'secret data' });
});

// Apply globally
app.use(requestTimer);
app.use(requestId);

Request and Response Objects

Express extends Node.js's built-in IncomingMessage and ServerResponse with additional properties and methods that make request handling much more ergonomic.

Request Object (req)

app.post('/api/example/:id', (req, res) => {
  // Route parameters (from URL pattern)
  req.params.id;         // '42'
  req.params;            // { id: '42' }

  // Query string (from ?key=value)
  req.query.page;        // '2'
  req.query.limit;       // '10'

  // Request body (requires express.json() middleware)
  req.body.name;         // 'John Doe'
  req.body;              // { name: 'John Doe', email: '...' }

  // Headers
  req.headers['authorization']; // 'Bearer eyJhb...'
  req.get('Content-Type');      // 'application/json'

  // Other useful properties
  req.method;     // 'POST'
  req.path;       // '/api/example/42'
  req.url;        // '/api/example/42?page=2'
  req.hostname;   // 'api.example.com'
  req.ip;         // '192.168.1.1'
  req.protocol;   // 'https'
  req.secure;     // true (if HTTPS)

  // Cookies (with cookie-parser middleware)
  req.cookies.sessionId;
  req.signedCookies.authToken;
});

Response Object (res)

app.get('/api/example', (req, res) => {
  // Send JSON response
  res.json({ success: true, data: [] });

  // Send with specific status code
  res.status(201).json({ created: true });
  res.status(404).json({ error: 'Not found' });

  // Send plain text or HTML
  res.send('Hello World');
  res.send('<h1>Hello</h1>');

  // Set headers
  res.set('X-Custom-Header', 'value');
  res.set({ 'X-A': '1', 'X-B': '2' });

  // Redirect
  res.redirect('/new-url');
  res.redirect(301, 'https://new-domain.com/path');

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

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

  // Set cookie
  res.cookie('token', jwtToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
  });

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

  // End without a body (e.g., for DELETE)
  res.status(204).end();
});

Building a REST API: Full CRUD Example

Let us build a complete RESTful API for a blog application with proper HTTP semantics, status codes, and input validation. This example uses in-memory storage for clarity; replace it with a database in production.

// src/routes/posts.js — Full CRUD REST API
const express = require('express');
const router = express.Router();

// In-memory store (replace with MongoDB/PostgreSQL in production)
let posts = [
  { id: 1, title: 'Getting Started with Express', content: 'Express is...', author: 'Alice', createdAt: new Date() },
  { id: 2, title: 'Middleware Deep Dive', content: 'Middleware is...', author: 'Bob', createdAt: new Date() },
];
let nextId = 3;

// GET /api/posts — List all posts (with pagination)
router.get('/', (req, res) => {
  const page = parseInt(req.query.page || '1', 10);
  const limit = parseInt(req.query.limit || '10', 10);
  const offset = (page - 1) * limit;

  const paginated = posts.slice(offset, offset + limit);

  res.json({
    data: paginated,
    meta: {
      total: posts.length,
      page,
      limit,
      totalPages: Math.ceil(posts.length / limit),
    },
  });
});

// GET /api/posts/:id — Get single post
router.get('/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id, 10));
  if (!post) {
    return res.status(404).json({ error: 'Post not found', id: req.params.id });
  }
  res.json({ data: post });
});

// POST /api/posts — Create a post
router.post('/', (req, res) => {
  const { title, content, author } = req.body;

  // Basic validation
  if (!title || !content || !author) {
    return res.status(400).json({
      error: 'Validation failed',
      details: {
        title: !title ? 'Title is required' : null,
        content: !content ? 'Content is required' : null,
        author: !author ? 'Author is required' : null,
      },
    });
  }

  const newPost = { id: nextId++, title, content, author, createdAt: new Date() };
  posts.push(newPost);

  res.status(201)
    .set('Location', '/api/posts/' + newPost.id)
    .json({ data: newPost });
});

// PUT /api/posts/:id — Replace entire post
router.put('/:id', (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id, 10));
  if (index === -1) {
    return res.status(404).json({ error: 'Post not found' });
  }

  const { title, content, author } = req.body;
  if (!title || !content || !author) {
    return res.status(400).json({ error: 'All fields required for PUT' });
  }

  posts[index] = { ...posts[index], title, content, author };
  res.json({ data: posts[index] });
});

// PATCH /api/posts/:id — Partial update
router.patch('/:id', (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id, 10));
  if (index === -1) {
    return res.status(404).json({ error: 'Post not found' });
  }

  const allowedFields = ['title', 'content', 'author'];
  const updates = Object.fromEntries(
    Object.entries(req.body).filter(([key]) => allowedFields.includes(key))
  );

  posts[index] = { ...posts[index], ...updates };
  res.json({ data: posts[index] });
});

// DELETE /api/posts/:id — Delete a post
router.delete('/:id', (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id, 10));
  if (index === -1) {
    return res.status(404).json({ error: 'Post not found' });
  }

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

module.exports = router;

Authentication with JWT and bcrypt

JSON Web Tokens (JWT) are the standard for stateless REST API authentication. Combined with bcrypt for password hashing, they provide a secure authentication layer without server-side session storage.

npm install jsonwebtoken bcrypt
npm install --save-dev @types/jsonwebtoken @types/bcrypt

User Registration and Login

// src/routes/auth.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const router = express.Router();

const SALT_ROUNDS = 12;
const JWT_SECRET = process.env.JWT_SECRET; // Never hardcode!
const JWT_EXPIRES_IN = '15m';
const REFRESH_TOKEN_EXPIRES_IN = '7d';

// Simulated user store (use database in production)
const users = [];

// POST /api/auth/register
router.post('/register', async (req, res, next) => {
  try {
    const { name, email, password } = req.body;

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

    if (password.length < 8) {
      return res.status(400).json({ error: 'Password must be at least 8 characters' });
    }

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

    // Hash password — NEVER store plain text passwords
    const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);

    const user = {
      id: users.length + 1,
      name,
      email,
      passwordHash,
      createdAt: new Date(),
    };
    users.push(user);

    // Don't return the hash
    res.status(201).json({ data: { id: user.id, name: user.name, email: user.email } });
  } catch (err) {
    next(err);
  }
});

// POST /api/auth/login
router.post('/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;

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

    const user = users.find(u => u.email === email);
    if (!user) {
      // Use same error message to prevent user enumeration
      return res.status(401).json({ error: 'Invalid credentials' });
    }

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

    // Sign access token (short-lived)
    const accessToken = jwt.sign(
      { sub: user.id, email: user.email, name: user.name },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );

    // Sign refresh token (long-lived, store in DB for rotation)
    const refreshToken = jwt.sign(
      { sub: user.id, type: 'refresh' },
      JWT_SECRET,
      { expiresIn: REFRESH_TOKEN_EXPIRES_IN }
    );

    // Store refresh token in httpOnly cookie (more secure than localStorage)
    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    });

    res.json({
      accessToken,
      user: { id: user.id, name: user.name, email: user.email },
    });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

JWT Authentication Middleware

// src/middleware/authenticate.js
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Authorization header missing or malformed' });
  }

  const token = authHeader.slice(7); // Remove 'Bearer '

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload; // Attach decoded payload to request
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    if (err.name === 'JsonWebTokenError') {
      return res.status(401).json({ error: 'Invalid token' });
    }
    next(err);
  }
}

// Optional authentication (does not require token but uses it if present)
function optionalAuthenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader) return next();

  try {
    const token = authHeader.slice(7);
    req.user = jwt.verify(token, process.env.JWT_SECRET);
  } catch {
    // Ignore invalid token for optional auth
  }
  next();
}

// Role-based authorization
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

module.exports = { authenticate, optionalAuthenticate, authorize };

// Usage in routes:
// app.get('/protected', authenticate, (req, res) => { ... });
// app.delete('/admin/users/:id', authenticate, authorize('admin'), (req, res) => { ... });

Error Handling

Proper error handling is critical for production Express apps. Express has a special convention for error handling: middleware with four arguments (err, req, res, next) is treated as an error handler and only invoked when next(err) is called.

Centralized Error Handler

// src/middleware/errorHandler.js

class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true; // Distinguish from programming errors
    Error.captureStackTrace(this, this.constructor);
  }
}

// Centralized error handling middleware — MUST be registered LAST
function errorHandler(err, req, res, next) {
  // Log error (use a proper logger like pino in production)
  console.error({
    message: err.message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
    requestId: req.id,
    method: req.method,
    url: req.url,
  });

  // Handle known operational errors
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
    });
  }

  // Handle Mongoose validation errors
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'Validation failed',
      details: Object.values(err.errors).map(e => e.message),
    });
  }

  // Handle JWT errors
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({ error: 'Invalid token' });
  }

  // Handle syntax errors in JSON body
  if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
    return res.status(400).json({ error: 'Invalid JSON in request body' });
  }

  // Handle database duplicate key errors (MongoDB)
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(409).json({ error: 'Duplicate value for ' + field });
  }

  // Generic server error — don't leak internal details in production
  const isDev = process.env.NODE_ENV === 'development';
  res.status(500).json({
    error: isDev ? err.message : 'Internal server error',
    ...(isDev && { stack: err.stack }),
  });
}

// 404 handler — place before errorHandler but after all routes
function notFound(req, res, next) {
  next(new AppError('Route ' + req.method + ' ' + req.path + ' not found', 404, 'NOT_FOUND'));
}

module.exports = { AppError, errorHandler, notFound };

Async Error Handling

In Express 4, unhandled promise rejections do not automatically reach the error handler. Use one of these three patterns:

// Pattern 1: try/catch (verbose but explicit)
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({ data: user });
  } catch (err) {
    next(err); // Pass to error handler
  }
});

// Pattern 2: asyncHandler wrapper (cleaner)
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

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

// Pattern 3: express-async-errors package (zero boilerplate)
// npm install express-async-errors
require('express-async-errors'); // Import once at the top of app.js

app.get('/users/:id', async (req, res) => {
  // Thrown errors and rejected promises automatically reach error handler
  const user = await User.findById(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json({ data: user });
});

// Express 5 (native async support — no wrapper needed)
// app.get('/users/:id', async (req, res) => { ... });  Works out of the box

Registering Middleware in the Right Order

// src/app.js — Correct middleware order
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const { errorHandler, notFound } = require('./middleware/errorHandler');

const app = express();

// 1. Security middleware (first)
app.use(helmet());
app.use(cors());

// 2. Logging
app.use(morgan('dev'));

// 3. Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 4. Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/posts', require('./routes/posts'));

// 5. 404 handler (after all routes)
app.use(notFound);

// 6. Error handler (MUST be last, 4 parameters)
app.use(errorHandler);

module.exports = app;

Express vs Fastify vs Koa vs Hapi

Choosing the right Node.js framework depends on your performance requirements, team familiarity, and ecosystem needs. Here is a comprehensive comparison of the four most popular options in 2026:

FeatureExpress 4/5Fastify 4Koa 2Hapi 21
Req/sec (benchmark)~28,000~76,000~50,000~30,000
TypeScript supportVia @types/expressBuilt-in (excellent)Via @types/koaVia @hapi/hapi types
Ecosystem / PluginsLargest (30M+ projects)Growing (800+ plugins)MediumMature (enterprise)
Learning curveVery easyEasy–MediumEasy (minimal)Steep
Schema validationManual / Joi / ZodBuilt-in (JSON Schema)Manual / JoiBuilt-in (Joi)
Async/awaitv5 native; v4 needs wrapperNativeNative (ctx-based)Native
OpenAPI / Swaggerswagger-ui-express@fastify/swagger (built-in)koa-swagger-decoratorhapi-swagger
Best forMost APIs, learningHigh-throughput APIsMinimalist projectsEnterprise, config-heavy
Weekly npm downloads30M+12M+1.5M+800K+

When to Choose Each Framework

  • Express.js — Best for most use cases. Choose when team familiarity, ecosystem size, available middleware, and community resources matter. The safe default for REST APIs and web apps.
  • Fastify — Choose when raw throughput is a measured requirement (high-volume microservices, data APIs). Built-in JSON schema validation and serialization provide both speed and structure.
  • Koa — Choose for small, focused projects where you want minimal overhead and thectx context object pattern. Created by the original Express team as a spiritual successor.
  • Hapi — Choose for enterprise projects where extensive configuration, built-in Joi validation, caching, authentication plugins, and a strict security model are valued over raw performance.

Production Setup and Project Structure

A well-structured Express project separates concerns and makes the codebase maintainable as it grows. Here is the recommended layout for a production API:

my-api/
├── src/
│   ├── app.js              # Express app setup (no listen)
│   ├── server.js           # HTTP server (calls app.listen)
│   ├── config/
│   │   ├── index.js        # Environment config with validation
│   │   └── database.js     # Database connection
│   ├── routes/
│   │   ├── index.js        # Route aggregator
│   │   ├── auth.js
│   │   ├── users.js
│   │   └── posts.js
│   ├── controllers/
│   │   ├── authController.js
│   │   ├── userController.js
│   │   └── postController.js
│   ├── services/
│   │   ├── authService.js  # Business logic
│   │   └── userService.js
│   ├── models/
│   │   ├── User.js         # Database models (Mongoose/Prisma)
│   │   └── Post.js
│   ├── middleware/
│   │   ├── authenticate.js
│   │   ├── authorize.js
│   │   ├── validate.js
│   │   └── errorHandler.js
│   └── utils/
│       ├── logger.js       # Pino logger
│       └── asyncHandler.js
├── tests/
│   ├── integration/
│   └── unit/
├── .env                    # Never commit!
├── .env.example            # Commit with empty values
├── .gitignore
├── package.json
└── Dockerfile

Environment Configuration

// src/config/index.js
require('dotenv').config();

const requiredEnvVars = ['JWT_SECRET', 'DATABASE_URL', 'NODE_ENV'];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error('Missing required environment variable: ' + envVar);
  }
}

module.exports = {
  port: parseInt(process.env.PORT || '3000', 10),
  nodeEnv: process.env.NODE_ENV,
  isDev: process.env.NODE_ENV === 'development',
  isProd: process.env.NODE_ENV === 'production',

  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '15m',
    refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
  },

  db: {
    url: process.env.DATABASE_URL,
    poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10),
  },

  cors: {
    origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
  },

  rateLimit: {
    windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10),
    max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
  },
};

Input Validation with Zod

Always validate and sanitize user input. Zod is the recommended validation library for TypeScript projects due to its excellent type inference and composable schema design.

npm install zod
// src/middleware/validate.ts
import { z, ZodSchema } from 'zod';
import { Request, Response, NextFunction } from 'express';

function validate(schema: ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });

    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.issues.map(issue => ({
          field: issue.path.join('.'),
          message: issue.message,
        })),
      });
    }

    // Replace with validated + coerced data
    req.body = result.data.body;
    next();
  };
}

// Define schemas
const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    password: z.string().min(8).max(128),
    role: z.enum(['user', 'admin']).default('user'),
  }),
});

const getUsersSchema = z.object({
  query: z.object({
    page: z.coerce.number().int().positive().default(1),
    limit: z.coerce.number().int().min(1).max(100).default(20),
    search: z.string().optional(),
  }),
  body: z.object({}),
  params: z.object({}),
});

// Usage in routes
router.post('/users', validate(createUserSchema), createUserController);
router.get('/users', validate(getUsersSchema), getUsersController);

Frequently Asked Questions

What is Express.js and why is it so popular?

Express.js is a minimal, unopinionated web framework for Node.js that provides routing, middleware support, and HTTP utilities. It is popular because of its small footprint, flexibility, and the enormous ecosystem of compatible npm middleware. With over 30 million weekly downloads, Express remains the most widely used Node.js framework in 2026.

Is Express.js still relevant in 2026?

Absolutely. While newer frameworks like Fastify offer better raw performance, Express wins on ecosystem maturity, community knowledge, available tutorials, and the sheer number of compatible middleware packages. For the vast majority of web APIs and services, Express performance is more than adequate — the bottleneck is almost always the database or network, not the framework.

How do I handle async errors in Express.js?

In Express 4, unhandled promise rejections do not reach the error handler automatically. Use a try/catch with next(err), wrap handlers with an asyncHandler utility, or install the express-async-errors package. Express 5 handles async errors natively.

What is the difference between app.use() and app.get()?

app.use() registers middleware that matches all HTTP methods and runs for every request matching a path prefix. app.get() registers a handler for GET requests at an exact path. Middleware must call next() to pass control forward; route handlers end the request/response cycle.

How do I structure a large Express.js application?

Use the MVC pattern: separate routes (routing layer), controllers (request handling logic), services (business logic), and models (data access). Mount express.Router() instances for each resource on the main app. Keep middleware in a dedicated folder and load environment configuration at startup with validation.

How does Express.js compare to Fastify in performance?

Fastify is typically 2–3x faster in benchmarks (~76K vs ~28K req/s). Fastify achieves this through built-in JSON schema compilation, faster routing, and optimized serialization. However, the performance gap rarely matters in practice since database queries dominate latency. Choose Express for ecosystem and ease; choose Fastify when throughput is a confirmed bottleneck.

What middleware is essential for production?

The essential production middleware stack is: helmet (security headers), cors,compression (gzip), express-rate-limit, a request logger (morgan orpino-http), express.json(), and a centralized error handler as the last app.use(). For authentication, add JWT middleware for protected routes.

Should I use Express.js or NestJS?

Use Express for smaller APIs, microservices, or projects where minimal boilerplate and flexibility are priorities. Use NestJS for large enterprise applications where opinionated architecture, dependency injection, and built-in TypeScript decorators provide long-term benefits. NestJS uses Express (or Fastify) under the hood, so the two are complementary rather than mutually exclusive.

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

{ }JSON FormatterJWTJWT DecoderB→Base64 Encoder

Related Articles

Node.jsガイド:バックエンド開発完全チュートリアル

Node.jsバックエンド開発をマスター。イベントループ、Express.js、REST API、JWT認証、DB統合、Jestテスト、PM2デプロイ、Node.js vs Deno vs Bun比較の完全ガイド。

JavaScript PromiseとAsync/Await完全ガイド

JavaScript PromiseとAsync/Awaitをマスター:作成、チェーン、Promise.all、エラー処理、並行実行パターン。

TypeScriptジェネリクス完全ガイド2026:基礎から高度なパターンまで

TypeScriptジェネリクスを完全マスター。型パラメータ、制約、条件型、マップ型、ユーティリティ型、実践パターンを解説。