Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services

Blog Posts

Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Back to all blogs

How to Secure an API: A Practical Checklist

Jun 10, 2026•7 min read

Securing an API is not one switch you flip — it's layers, each catching what the last one missed. This is defense in depth: HTTPS, authentication, authorization, rate limiting, validation, safe errors, logging, and secret management, stacked so that a single failure doesn't hand over the whole system. The industry reference for what actually goes wrong is the OWASP API Security Top 10, and most breaches I've seen trace back to one of its items — usually a missing authorization check. I've built and hardened APIs for e-commerce apps moving real money, and this is the checklist I run down every time.

This is a deep dive under my full-stack app guide. It pairs closely with how to authenticate users — auth is the layer, this is the whole wall.

1. HTTPS/TLS everywhere

Non-negotiable, first layer. Without TLS, every request — credentials, tokens, personal data — travels in plaintext that anyone on the network path can read. Serve only over HTTPS, redirect HTTP to HTTPS, and tell browsers to never downgrade with HSTS:

js
1app.use((req, res, next) => { 2 res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') 3 next() 4})

Modern hosts and load balancers handle TLS termination for you. There's no excuse to run plain HTTP in production — none.

2. Authentication: prove who's calling

Every non-public endpoint requires authentication. Three mechanisms, for different callers:

  • OAuth 2.0 / JWT — for users and delegated access (covered in the auth guide).
  • API keys — for server-to-server. Critical distinction: API keys identify an application, not a user. They say "this app is calling," not "this person is authorized." Don't use a key as proof of user identity.

Treat keys like passwords: hash them at rest, scope them to the minimum needed, and make them revocable.

js
1app.use('/api', async (req, res, next) => { 2 const key = req.header('x-api-key') 3 const app = key && await lookupByKeyHash(hash(key)) 4 if (!app) return res.status(401).json({ error: 'Unauthorized' }) 5 req.app = app 6 next() 7})

3. Authorization: enforce what they can do

Authentication is who you are; authorization is what you may touch. This is where APIs get breached most, so it earns its own layer.

RBAC gates actions by role. But the failure that dominates the OWASP list is Broken Object Level Authorization (BOLA) — also called IDOR. The user is authenticated, but the API returns someone else's object because it never checks ownership. Changing /orders/1001 to /orders/1002 should not reveal another customer's order.

js
1app.get('/api/orders/:id', requireAuth, async (req, res) => { 2 const order = await getOrder(req.params.id) 3 // the check that stops BOLA/IDOR: 4 if (!order || order.ownerId !== req.user.id) { 5 return res.status(404).json({ error: 'Not found' }) 6 } 7 res.json(order) 8})

Every endpoint that takes an ID needs an ownership check. There is no shortcut, and no client-side substitute.

4. Rate limiting

Without limits, one client can brute-force logins, scrape your whole dataset, or knock the service over. Cap requests per client and return 429 Too Many Requests when they exceed it:

js
1import rateLimit from 'express-rate-limit' 2 3app.use('/api', rateLimit({ 4 windowMs: 15 * 60 * 1000, // 15 minutes 5 max: 100, // per IP per window 6 standardHeaders: true, 7 message: { error: 'Too many requests' }, 8})) 9 10// tighter limit on the endpoint attackers hammer 11app.use('/api/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }))

Login and password-reset routes deserve stricter limits than the rest of the API.

5. Input validation and schema

Never trust input — validate every field against a schema at the boundary, and reject anything that doesn't match with a 400. This closes off injection, type-confusion, and malformed-payload bugs. I use a schema validator like Zod:

js
1import { z } from 'zod' 2 3const CreateTask = z.object({ 4 title: z.string().min(1).max(200), 5 dueAt: z.string().datetime().optional(), 6}) 7 8app.post('/api/tasks', requireAuth, (req, res) => { 9 const parsed = CreateTask.safeParse(req.body) 10 if (!parsed.success) return res.status(400).json({ error: parsed.error.issues }) 11 // parsed.data is now typed and safe 12})

And always use parameterized database queries — never string-concatenate input into SQL. Validation and parameterized queries together shut down injection.

6. Safe error handling

Errors must not leak internals. A stack trace, SQL string, or file path in a response is a map of your system handed to an attacker. Return a generic message and a status code; log the detail server-side where only you can see it:

js
1app.use((err, req, res, next) => { 2 logger.error({ err, path: req.path, userId: req.user?.id }) // full detail, server-side 3 res.status(err.status || 500).json({ error: 'Something went wrong' }) // generic, to client 4})

7. Logging and monitoring

You can't respond to what you can't see. Log authentication events, authorization failures, and anomalies — then actually alert on them. A spike in 401s or 429s is often an attack in progress. Log the events, never the secrets: no passwords, tokens, or full card numbers in your logs.

8. API gateway

For anything beyond a single service, put a gateway in front. It centralizes TLS termination, authentication, rate limiting, and logging in one place instead of scattering them across every service — one consistent enforcement point, and one place to update policy.

9. Secret management

Secrets — database URLs, API keys, signing keys — never live in code or Git. A key committed to a repo is compromised the moment it's pushed, and rewriting history rarely fully removes it.

  • Load secrets from environment variables or a secrets manager (Vault, AWS Secrets Manager, Doppler).
  • Add .env to .gitignore and commit a .env.example with blank values.
  • Rotate secrets regularly and immediately after any suspected exposure.
bash
1# .gitignore 2.env 3.env.local

If a secret ever lands in Git, rotate it — don't just delete the line and hope.

10. Continuous security testing

Security isn't a one-time pass. Run dependency scanning (npm audit, Dependabot) so a vulnerable package doesn't sneak in, add static analysis to CI, and test your own endpoints for BOLA by trying to access objects you don't own. Make it part of the pipeline, not a heroic quarterly effort.

Reality check: what does NOT secure your API

These myths get people breached, so I say them plainly:

  • CORS is not security. CORS controls which browser origins can read responses. It does nothing against curl, a script, or a server making a direct request. It is not an access-control mechanism. (More in connect React to Node.)
  • You cannot hide a public API. If a browser or mobile app can call it, the endpoint, its shape, and any embedded keys are discoverable. Obscurity is not protection.
  • Client-side checks are not enforcement. Disabling a button or validating in React is UX. Every rule must be re-checked on the server, because the client can be bypassed entirely.

Security lives on the server. Always.

The checklist

Run this before you ship, and again on every review:

#ControlGuards againstStatus
1HTTPS/TLS + HSTSEavesdropping, MITM☐
2Authentication on every private routeAnonymous access☐
3RBAC + object-level ownership checksBOLA / IDOR☐
4Rate limiting → 429Brute force, scraping, DoS☐
5Schema validation + parameterized queriesInjection, bad input☐
6Generic error responsesInternal info leaks☐
7Logging + alerting (no secrets logged)Blind spots☐
8API gatewayInconsistent enforcement☐
9Secrets in a vault, rotated, never in GitCredential leaks☐
10Dependency + endpoint security testingKnown CVEs, regressions☐

Where to go next

The authentication and authorization layers here go deeper in how to authenticate users in a web application — read it alongside this, since the two overlap. For where security fits in the full build, head back to the full-stack app guide.

Related Posts

How to Build a Full-Stack App: A Step-by-Step Guide

The exact path I follow to ship a full-stack app end to end — data model, Node/Express API, database, React frontend, auth, security, and deploy — with links to the deep dives for each step.

How to Authenticate Users in a Web Application

Authentication has four parts: verifying identity, managing sessions, authorizing actions, and expiring access safely. I cover password hashing, sessions vs JWT, HttpOnly cookies, OAuth, passkeys, and the architecture I actually recommend.

How to Connect a React Front End to a Node.js Back End

A React app and a Node.js API are two separate programs talking over HTTP. I show the Express server, the React fetch, why CORS exists, how to set up a dev proxy, project structure, and how to run and deploy both.