A slow APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is a broken API from your users' perspective. Performance in Express boils down to two things: sending less data, and doing less work per request. The techniques in this lesson address both, and most of them take under ten minutes to add to an existing app.
Compression
Every byte you send over the network costs time. The compression middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. runs your response through Gzip (or Brotli, with a bit more config) before it leaves the server. For JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. APIs, this typically shrinks responses by 60-80%.
import compression from 'compression';
app.use(compression({
threshold: 1024, // Only compress responses larger than 1KB
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false; // Allow clients to opt out
}
return compression.filter(req, res);
}
}));compression near the top of your middleware stack, before your routes. If you add it after, routes that send their response before the middleware runs won't get compressed.Setting NODE_ENV
Express uses process.env.NODE_ENV to decide how much overhead to spend on development conveniences. In development mode, it does things like recompile templates on every request and generate detailed error pages. Set it to production and it caches templates and skips that work.
# In your process manager or hosting environment
NODE_ENV=production node index.jsThis single environment variableWhat is environment variable?A value stored outside your code that configures behavior per deployment, commonly used for secrets like API keys and database URLs. is the lowest-effort performance win available. Many popular libraries (not just Express) make similar optimizations when they see it.
HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. caching
HTTP has a built-in caching system. Your server tells the client (and any CDNs or proxies in between) how long a response is valid, and they reuse it without hitting your server again. This is free performance, you do zero work for cached requests.
// Reusable middleware factory
const cacheFor = (seconds) => (req, res, next) => {
res.set('Cache-Control', `public, max-age=${seconds}`);
next();
};
// Public data - cache aggressively
app.get('/api/public-data', cacheFor(3600), handler);
// User-specific data - never cache
app.get('/api/user/profile', (req, res, next) => {
res.set('Cache-Control', 'no-store');
next();
}, handler);| Cache-Control value | Use it when |
|---|---|
public, max-age=3600 | Public data that changes rarely (product listings, etc.) |
private, max-age=300 | User-specific data that can be cached in the browser only |
no-cache | Content that must be revalidated with the server each time |
no-store | Sensitive data that must never be cached anywhere |
PaginationWhat is pagination?Splitting a large set of results into smaller pages so the server and client only handle a manageable chunk at a time. and lean queries
Loading ten thousand database records to return the first twenty is a common performance killer. Always paginate, and always select only the fields you need.
app.get('/api/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Cap at 100
const skip = (page - 1) * limit;
const users = await User
.find()
.select('-password -secretField') // Exclude sensitive fields
.limit(limit)
.skip(skip)
.lean(); // Return plain JS objects, not full Mongoose documents
res.json(users);
});.lean() is a Mongoose-specific optimization worth remembering. Full Mongoose documents carry a lot of extra functionality (change tracking, validation, etc.) that you don't need when you're just reading data to send as JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it.. .lean() skips all that and returns plain objects, which are significantly faster to create and serialize.
Connection pooling
Opening a new database connection for every request is expensive, it involves a network handshakeWhat is handshake?The initial exchange between a client and server that establishes a connection and agrees on communication rules before data starts flowing., authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token., and memory allocation. Connection pooling keeps a set of connections open and ready, and your requests borrow one from the pool rather than creating a new one.
// MongoDB with Mongoose
await mongoose.connect(process.env.DATABASE_URL, {
maxPoolSize: 10, // Maximum concurrent connections
minPoolSize: 5, // Keep at least 5 connections open
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
});
// PostgreSQL with pg
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});Quick reference
| Optimization | Impact | Effort |
|---|---|---|
NODE_ENV=production | Medium | Very low |
compression middleware | High (60-80% smaller) | Low |
| HTTP caching headers | Very high for read-heavy APIs | Low |
Pagination + .select() | High for large datasets | Low |
| Connection pooling | High under load | Low |
| Database indexes | Very high for slow queries | Medium |