Building an API is only half the job.
Once an API is exposed to real users, you have to think about what happens when someone sends unexpected input, calls an endpoint they shouldn't, sends thousands of requests, or gets access to a secret.
That's why API security isn't a single feature. It's a collection of layers that protect different parts of the application.
A typical request might go through:
Request
↓
Authentication
↓
Authorization
↓
Validation
↓
Rate Limiting
↓
Business Logic
↓
Database
↓
Response
Let's look at the important parts.
Authentication
The first question is:
Who is making this request?
For example:
GET /api/profile
Authorization: Bearer <token>
The backend verifies the credentials and identifies the user.
If authentication fails:
Invalid / missing credentials
↓
401 Unauthorized
Authentication establishes identity, but it doesn't decide what that user can access.
That's authorization's job.
Authorization
Suppose two users are logged in:
Alex → user
Sarah → admin
Alex sends:
DELETE /api/users/100
The backend shouldn't simply check whether Alex is logged in.
It should also check whether Alex has permission to delete that user.
Authenticated user
↓
Permission check
↓
Allowed?
├── No → 403 Forbidden
└── Yes → Continue
This check must happen on the backend.
Hiding a button in the frontend is not authorization.
A user can always send the API request directly.
Validate Client Input
Never assume that data from the frontend is trustworthy.
A client might send:
{
"name": 123,
"age": -50,
"role": "admin"
}
even if your frontend never allows those values.
The backend should validate:
Required fields
Data types
Formats
Lengths
Allowed values
Business rules
A useful rule is:
Frontend validation improves the user experience. Backend validation protects the application.
SQL Injection
One classic API vulnerability is SQL injection.
This is dangerous:
const query =
`SELECT * FROM users WHERE email = '${email}'`;
User input is being inserted directly into the SQL statement.
Instead, use parameterized queries:
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
The database can then treat the supplied value as data rather than SQL syntax.
The same principle applies when using ORMs: use their parameterized query mechanisms rather than constructing raw SQL from untrusted input.
XSS
XSS stands for Cross-Site Scripting.
Consider a comment system:
User input
↓
API
↓
Database
↓
Browser
If the application later renders that content as executable HTML or JavaScript, an attacker could potentially run code in another user's browser.
Common defenses include:
Safe rendering
Output encoding
Avoiding unsafe HTML injection
Content Security Policy
The important lesson is that data stored in your database isn't automatically safe.
It may have originally come from an untrusted user.
CSRF
CSRF stands for Cross-Site Request Forgery.
It is particularly relevant when authentication uses cookies because browsers can automatically send cookies with requests.
A simplified attack looks like:
User is logged in
↓
Authentication cookie
↓
Malicious website
↓
Forged request
↓
Your API
Depending on the application, protections can include:
SameSite cookies
CSRF tokens
Origin validation
The right defense depends on how authentication and browser requests are designed.
Rate Limiting
An attacker can automate requests to endpoints such as:
Login
OTP verification
Password reset
Search
Expensive API operations
Without limits:
Attacker
↓
Thousands of requests
↓
Backend
↓
Database
Rate limiting can stop excessive traffic:
Too many requests
↓
429 Too Many Requests
Rate limiting isn't only about preventing brute-force attacks. It can also protect backend resources from accidental or malicious traffic spikes.
Protect Your Secrets
Backend applications often use sensitive credentials:
Database passwords
JWT signing keys
API keys
Cloud credentials
Payment provider secrets
OAuth secrets
Don't put them directly into source code:
const secret = "my-secret-key";
Use environment configuration or a dedicated secret-management system instead:
const secret = process.env.JWT_SECRET;
Also remember that secrets can leak through more than source code.
Watch out for:
Git repositories
Logs
Error messages
Frontend bundles
Debug endpoints
Screenshots
A secret is only useful if it stays secret.
HTTPS and Encryption
API traffic should use HTTPS.
Without HTTPS:
Client
↓
Network
↓
Server
With HTTPS:
Client
↓
Encrypted TLS connection
↓
Server
HTTPS protects data while it travels between the client and server.
This is especially important for:
Passwords
Cookies
Tokens
Personal data
Payment information
Encryption protects the connection, but it doesn't decide whether the authenticated user has permission to access a resource.
Security Headers
HTTP security headers can provide additional browser protections.
Common examples include:
Content-Security-Policy
Strict-Transport-Security
X-Content-Type-Options
Referrer-Policy
For example, Content Security Policy can restrict where a browser is allowed to load scripts and other resources.
These headers don't replace secure application code, but they add another layer of defense.
Don't Return More Data Than Necessary
Suppose your database contains:
id
name
email
password_hash
internal_notes
created_at
Your API probably doesn't need to return all of it.
Instead of:
SELECT * FROM users;
select only what the client needs:
SELECT id, name, email
FROM users;
The same applies to error messages.
Avoid exposing:
Database credentials
SQL queries
Internal file paths
Stack traces
Private service information
The client usually needs a simple error, while detailed information can remain in protected server logs.
Don't Trust IDs From the Client
Consider:
GET /api/orders/123
The user is authenticated.
That's not enough.
The backend should check whether that particular order belongs to that user.
Otherwise, someone might change:
/api/orders/123
to:
/api/orders/124
and access another user's data.
The check should be:
Authenticated user
↓
Requested resource
↓
Does the user have access?
↓
Allow / Reject
This is a common authorization problem in APIs.
Logging and Monitoring
Security also means being able to notice suspicious activity.
Useful events to track include:
Failed logins
Permission failures
Password reset attempts
Rate-limit violations
Administrative actions
Unusual request patterns
For example:
User 42
DELETE /api/users/100
Authorization denied
But don't casually log sensitive credentials.
Avoid storing things such as:
Passwords
Access tokens
Refresh tokens
API keys
Logs can become a security risk themselves if they contain secrets.
Defense in Depth
At this point, the bigger picture becomes clear.
API security isn't about finding one perfect security mechanism.
It's about having several layers:
Request
↓
Rate Limiting
↓
Authentication
↓
Authorization
↓
Validation
↓
Business Logic
↓
Database
↓
Response
And around the application:
HTTPS
Security Headers
Secret Management
Logging
Monitoring
Each layer deals with a different type of problem.
For example:
SQL Injection
→ Parameterized queries
XSS
→ Safe rendering + output handling
CSRF
→ Appropriate CSRF protections
Brute Force
→ Rate limiting
Broken Authorization
→ Server-side permission checks
Exposed Secrets
→ Secret management
If one layer fails, another layer can still limit the damage.
That's the basic idea behind defense in depth.
A Practical Checklist
Before putting an API into production, ask:
Authentication
[ ] Are protected endpoints authenticated?
Authorization
[ ] Are permissions checked on the server?
[ ] Can users access other users' resources?
Validation
[ ] Are request bodies validated?
[ ] Are query parameters validated?
Database
[ ] Are queries parameterized?
Rate Limiting
[ ] Are login and expensive endpoints protected?
Secrets
[ ] Are credentials outside the source code?
[ ] Are secrets excluded from logs?
Transport
[ ] Is HTTPS enabled?
Browser Security
[ ] Are cookies configured appropriately?
[ ] Are CSRF protections considered?
Monitoring
[ ] Are suspicious activities detectable?
This isn't a complete security audit, but it's a good starting point.
The Mental Model
When securing an API, think about the request one question at a time:
Who is making the request?
↓
Authentication
What can they access?
↓
Authorization
Is the input valid?
↓
Validation
Are they sending too many requests?
↓
Rate Limiting
Can the connection be intercepted?
↓
HTTPS
Can browser behavior be abused?
↓
CSRF / Security Headers
Are credentials protected?
↓
Secret Management
Can suspicious behavior be detected?
↓
Logging / Monitoring
The main lesson is simple:
API security isn't a feature you add once. It's a collection of decisions made throughout the backend.
Authentication tells you who the user is.
Authorization controls what they can access.
Validation keeps unexpected input out of your application logic.
Rate limiting controls abusive traffic.
HTTPS protects data in transit.
And careful secret management prevents credentials from becoming an easy attack path.
The most useful question isn't:
"Is my API secure?"
It's:
"What could go wrong at each stage of this request, and what prevents it?"
That's the mindset that helps turn a working API into a secure one.
Top comments (0)