Exposing an API to a decoupled frontend requires consistent security discipline. Frameworks such as Laravel, Django, and NestJS provide many useful building blocks, but no default configuration replaces a properly designed architecture.
These are the rules I prioritize.
1. Authentication
An API must clearly distinguish authentication from authorization.
Authentication answers:
Who are you?
Authorization answers:
Are you allowed to perform this operation?
An authenticated user should therefore not automatically have access to every resource.
JWT or HTTP-Only cookies?
Both approaches can be appropriate depending on the architecture.
For a web application within the same ecosystem, properly configured HttpOnly, Secure cookies can be very practical.
For APIs consumed by several clients, tokens can be appropriate.
In every case, the goal is to avoid fragile secret handling in the browser.
2. Strict CORS
CORS is not an authentication mechanism.
It controls which origins can perform certain browser requests.
Production configurations should avoid blindly using:
Access-Control-Allow-Origin: *
when the API handles private data or credentials.
I prefer explicitly defining allowed origins.
3. Input validation
Every external payload should be treated as untrusted.
I validate:
- types;
- lengths;
- formats;
- enums;
- relationships;
- permissions.
For example:
const CreateUser = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
});
Server-side validation is still required even if the frontend already has a schema.
4. Rate limiting
Sensitive endpoints deserve special attention:
- login;
- password reset;
- OTP;
- expensive searches;
- uploads;
- resource generation;
- public webhooks.
Rate limiting reduces abuse and protects server resources.
5. SQL and ORMs
Using an ORM does not automatically make an application secure.
I prefer parameterized ORM mechanisms and avoid concatenating user input into SQL queries.
For raw SQL, parameters should remain separate from data.
6. Resource-level authorization
An API can have correct authentication and still have broken authorization.
For example:
GET /api/orders/8421
The server should not only check whether the user is logged in. It must verify that the user is allowed to access order 8421.
That distinction is fundamental.
7. Secrets and logs
I never commit:
- API keys;
- passwords;
- tokens;
- JWT secrets;
to Git.
I also treat logs carefully. An overly detailed debug log can become a data leak.
8. Defense in depth
My strategy is never:
“I have JWT, therefore my API is secure.”
I combine:
Authentication
↓
Authorization
↓
Validation
↓
Rate limiting
↓
Database constraints
↓
Monitoring
Each layer reduces the impact of failures elsewhere.
9. Production
Before considering an API production-ready, I check:
- HTTPS;
- security headers;
- CORS;
- session/token expiration;
- permissions;
- server-side validation;
- request-size limits;
- error handling;
- backups;
- monitoring.
Security is not a feature added at the end. It is part of the architecture from the beginning.
