Security isn't something you add to an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. after it works, it's something you build in from the start. The good news is that most API security comes down to a handful of well-understood patterns. Get these right and you've covered the vast majority of real-world attack surface.
Transport security
Everything starts with HTTPSWhat is https?HTTP with encryption added, so data traveling between your browser and a server can't be read or tampered with by anyone in between.. 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. sends your data in plaintext, anyone on the same network can read it. HTTPS encrypts the connection. In production, there is no reason to serve an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. over HTTP.
// Never in production
http://api.example.com/users
// Always
https://api.example.com/usersBeyond HTTPS, add security headers to every response. These headers tell browsers and clients how to treat your responses:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'Strict-Transport-Security tells browsers to only ever connect to your domain over HTTPS, even if someone types http://. It's enforced by the browser for up to a year after it first sees the header.
AuthenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
Authentication answers: "Who are you?" Three common approaches:
APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. keys
Simple key passed in a header. Good for server-to-server communication and internal APIs:
GET /api/data
X-API-Key: your-api-key-hereThe downside: if the key leaks, you have to rotate it everywhere. No expiry, no claims, no scoping without extra infrastructure.
JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup. (JSON Web Tokens)
The industry standard for statelessWhat is stateless?A design where each request contains all the information the server needs, so any server can handle any request without remembering previous ones. APIs. The client authenticates once, receives a tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use., and sends it with every subsequent request:
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...The token itself contains claims, the user's ID, roles, expiry time, encoded and signed. The server verifies the signature without hitting the database. Tokens expire, which limits the damage from leaks.
OAuth 2.0What is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password.
When your API needs to act on behalf of a user across third-party systems. The flows depend on the client type:
| Flow | Client type | Use case |
|---|---|---|
| Authorization Code | Web app | "Login with Google" |
| PKCE | Mobile / SPA | Same, without client secret |
| Client Credentials | Server-to-server | Service accounts, background jobs |
AuthorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages.
AuthenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. proves identity. Authorization decides what that identity is allowed to do. Confusing these is a common source of bugs.
Role-based access controlWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions.
The simplest model: users have roles, roles have permissions. A 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. check before sensitive routes:
function requireRole(role) {
return (req, res, next) => {
if (!req.user.roles.includes(role)) {
return res.status(403).json({
error: 'Forbidden: insufficient permissions'
});
}
next();
};
}
app.delete('/users/:id', requireRole('admin'), deleteUser);Resource-level authorization
Beyond roles, check that the authenticated user is allowed to access this specific resource. A common mistake is forgetting this check, a logged-in user can view another user's private data just by guessing their ID:
app.get('/users/:id', async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({
error: 'Cannot access another user\'s data'
});
}
// ... fetch user
});Input validation
Never trust client input. Validate the shape, type, and value of everything the server receives, even from your own frontend:
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150),
role: Joi.string().valid('user', 'admin').default('user')
});
app.post('/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(422).json({
error: 'Validation failed',
details: error.details
});
}
// use `value`, not `req.body` - it's been sanitized
});Beyond shape validation, use parameterized queries to prevent SQL injectionWhat is sql injection?An attack where user input is inserted directly into a database query, letting the attacker read, modify, or delete data. Parameterized queries prevent it.. Never concatenate user input into a query stringWhat is query string?The part of a URL after the ? that carries optional key-value pairs (like ?page=2&limit=10) used for filtering, sorting, or pagination.:
// SQL injection waiting to happen
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
// Parameterized - safe
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.params.id]);Rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed.
Rate limiting caps how many requests a client can make in a time window. It protects against brute force attacks on auth endpoints, scrapers, and accidental infinite loops in client code:
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100
});
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 login attempts per 15 minutes
skipSuccessfulRequests: true
});
app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);Set much stricter limits on login and password-reset endpoints than on general APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpoints. When a limit is hit, return 429 Too Many Requests with a Retry-After header.
CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. configuration
CORS (Cross-Origin Resource Sharing) controls which websites can make requests to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. from a browser. The temptation is to set Access-Control-Allow-Origin: * to make everything work, resist it for authenticated endpoints:
const corsOptions = {
origin: [
'https://app.example.com',
'https://admin.example.com'
],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));credentials: true is required for cookies or AuthorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. headers to work cross-originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443.. When you use this, you cannot use * as the origin, you must specify exact domains.
Quick reference
| Layer | Tool | What it prevents |
|---|---|---|
| Transport | HTTPS + HSTS | Eavesdropping, MITM |
| Authentication | JWT / OAuth | Unauthorized access |
| Authorization | RBAC + resource checks | Privilege escalation, BOLA |
| Input validation | Schema validation | Injection attacks, bad data |
| Injection | Parameterized queries | SQL/NoSQL injection |
| Rate limiting | Request quotas | Brute force, DDoS |
| CORS | Origin allowlist | Cross-site request forgery |