REST API Security Checklist: 12 Practical Ways to Secure a Node.js API
A practical REST API security checklist covering authentication, authorization, input validation, rate limiting, secrets, security headers, error handling, logging, and common Node.js API mistakes

REST API Security Checklist: 12 Practical Ways to Secure a Node.js API
Building a REST API is only half the job.
Once an API accepts requests from browsers, mobile apps, or third-party clients, it becomes an important security boundary. A poorly protected endpoint can expose private data, allow unauthorized changes, or provide attackers with a way to abuse application functionality.
The good news is that API security does not require turning every project into a giant security system.
You can start with a practical checklist covering the most important areas:
- Authentication
- Authorization
- Input validation
- Rate limiting
- Secret management
- HTTPS
- Security headers
- Error handling
- Logging
- Dependency security
- CORS configuration
- API testing
This guide uses Node.js and Express examples, but most of the principles apply to REST APIs built with other backend technologies.
Quick Security Checklist
Before deploying a REST API, ask:
| Security Area | Question |
|---|---|
| Authentication | Can the API verify who the user is? |
| Authorization | Can the API verify what that user is allowed to do? |
| Input validation | Are incoming values validated on the server? |
| Rate limiting | Can one client send unlimited requests? |
| Secrets | Are passwords, tokens, and keys kept out of source code? |
| HTTPS | Is sensitive traffic encrypted in transit? |
| CORS | Are only appropriate browser origins allowed? |
| Errors | Do error responses avoid exposing internal details? |
| Logging | Can suspicious activity be investigated? |
| Dependencies | Are packages regularly checked for known vulnerabilities? |
| Data access | Can users access only records they are permitted to access? |
| Testing | Have security-sensitive endpoints been tested directly? |
A secure API is not created by checking one item. Security comes from applying several controls together.
1. Understand Authentication vs Authorization
These two concepts are often confused.
Authentication
Authentication answers:
Who are you?
For example:
User submits login credentials
|
v
Server verifies credentials
|
v
User is authenticatedAuthorization
Authorization answers:
What are you allowed to do?
For example:
Authenticated User
|
v
Request: DELETE /api/users/25
|
v
Is this user allowed to delete user 25?
|
+--+--+
| |
Yes No
| |
v v
Delete 403A login system does not automatically mean the API is secure.
Imagine a user is authenticated as:
User ID: 10and sends:
GET /api/orders/55The API must still determine whether order 55 belongs to user 10 or whether the user has another legitimate reason to access it.
Authentication establishes identity.
Authorization establishes permission.
2. Never Trust the Frontend
One of the most important API security rules is simple:
The client is not a trusted environment.
Suppose your frontend disables an "Admin" button for normal users.
That does not protect the API.
A user could manually send:
DELETE /api/products/15without clicking anything in your interface.
The server must perform the authorization check.
Bad approach:
if (userIsAdminFromFrontend) {
showAdminButton();
}Better approach:
app.delete("/api/products/:id", authenticate, requireAdmin, deleteProduct);The important check happens on the server.
3. Protect Resources With Authorization
Consider this endpoint:
GET /api/users/:idA dangerous implementation might simply return any requested user:
app.get("/api/users/:id", async (req, res) => {
const user = await findUser(req.params.id);
res.json(user);
});If user 10 can request:
/api/users/11the API might expose another user's information.
Instead, the application should enforce an authorization rule.
A simplified example:
app.get("/api/users/:id", authenticate, async (req, res) => {
const requestedId = Number(req.params.id);
if (req.user.id !== requestedId && req.user.role !== "admin") {
return res.status(403).json({
error: "Forbidden"
});
}
const user = await findUser(requestedId);
if (!user) {
return res.status(404).json({
error: "User not found"
});
}
res.json(user);
});The exact implementation depends on your authentication system and data model, but the principle remains the same:
Do not allow access simply because someone knows an ID.
4. Validate Every Important Input
APIs receive data from outside the server.
That data should be treated as untrusted until validated.
For example:
app.post("/api/tasks", (req, res) => {
const { title } = req.body;
if (typeof title !== "string" || title.trim().length < 3) {
return res.status(400).json({
error: "Title must contain at least 3 characters"
});
}
// Continue processing...
});Validation should consider:
- Type
- Required fields
- Length
- Format
- Allowed values
- Numeric ranges
- Relationships between fields
For example, if an API accepts:
{
"age": 25
}do not assume the client will always send a number.
Someone could send:
{
"age": "twenty-five"
}or:
{
"age": -999
}Server-side validation should define what the application actually accepts.
5. Avoid SQL Injection
If your API communicates with a relational database, never construct SQL queries by directly concatenating user input.
Dangerous pattern:
const query =
"SELECT * FROM users WHERE email = '" +
req.body.email +
"'";User-controlled input is being inserted directly into the query.
Use parameterized queries or the safe query mechanisms provided by your database library.
Conceptually:
const query = "SELECT * FROM users WHERE email = ?";
const values = [req.body.email];The exact syntax depends on your database driver.
The same principle applies to other interpreters and data-processing systems:
Do not treat untrusted input as executable instructions.
6. Never Hardcode Secrets
Avoid putting credentials directly into source code:
const databasePassword = "MySecretPassword123";
const jwtSecret = "my-secret-key";This creates problems when the code is committed to Git or shared with another developer.
Instead, use environment variables.
For example:
DATABASE_URL=...
JWT_SECRET=...Then access them from the server:
const databaseUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;A local development environment can use a .env file with an appropriate environment-variable loader.
Do not commit your real .env file to a public repository.
A .gitignore entry commonly includes:
node_modules/
.envIf a secret has already been exposed publicly, simply deleting the line from the latest commit is not enough. Treat the secret as compromised and rotate it.
7. Use HTTPS in Production
Sensitive API communication should be protected in transit.
Without encryption, credentials, session information, and other sensitive data may be exposed to someone capable of observing network traffic.
Production applications should therefore use HTTPS.
For example:
https://api.example.com/usersis preferable to sending sensitive production traffic over plain HTTP.
HTTPS does not replace authentication or authorization.
It protects communication in transit.
You still need controls on the server.
8. Configure CORS Carefully
Cross-Origin Resource Sharing, or CORS, controls which browser origins are permitted to make cross-origin requests under the browser's CORS rules.
A common beginner mistake is allowing every origin:
app.use(cors({
origin: "*"
}));Whether this is appropriate depends on the API.
For a private application, a narrower configuration may be more appropriate:
app.use(cors({
origin: "https://app.example.com"
}));For multiple trusted frontend applications, configure the allowed origins deliberately.
Do not assume CORS is an authentication system.
CORS primarily controls browser behavior. It does not stop a malicious client such as a script, server, or API testing tool from sending requests directly to your server.
Your API still needs authentication and authorization.
9. Add Rate Limiting
An endpoint such as:
POST /api/loginshould not necessarily accept unlimited requests from one client.
Without appropriate limits, attackers may repeatedly attempt:
- Password guessing
- Credential stuffing
- Automated requests
- Resource-heavy operations
- Repeated form submissions
Rate limiting can restrict how frequently requests are accepted.
Conceptually:
Client
|
| 100 requests
v
Rate Limiter
|
+---- Within limit ----> API
|
+---- Over limit ------> 429 Too Many RequestsA production Express application can use a maintained rate-limiting package rather than implementing a simplistic counter from scratch.
For example, after installing an appropriate package:
const rateLimit = require("express-rate-limit");
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
standardHeaders: "draft-8",
legacyHeaders: false
});
app.use("/api/login", loginLimiter);The exact limits should depend on the endpoint and application.
A login endpoint and a public product-list endpoint usually should not have identical limits.
10. Do Not Expose Sensitive Error Details
During development, detailed errors are useful.
For example:
console.error(error);But returning internal details directly to clients can reveal information about your application.
Avoid responses such as:
{
"error": "MongoServerError: connection failed at /app/database/client.js:72"
}A safer public response might be:
{
"error": "Internal server error"
}The server can still log the detailed error internally.
A useful pattern is:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
error: "Internal server error"
});
});The goal is to give the client enough information to understand the result without unnecessarily revealing internal implementation details.
11. Add Security Headers
HTTP response headers can provide useful browser-level security controls.
For Express applications, a commonly used approach is the helmet package.
Install it:
npm install helmetThen:
const helmet = require("helmet");
app.use(helmet());Security headers can help reduce exposure to several classes of browser-related attacks and unsafe behavior.
However, adding a security-header package is not a replacement for secure application design.
You still need:
- Authentication
- Authorization
- Input validation
- Secure session handling
- Safe database queries
- Proper secret management
Think of security headers as one layer of defense.
12. Keep Dependencies Updated
Node.js applications often depend on many third-party packages.
That creates another security consideration: your application inherits risk from its dependencies.
Start by checking the project:
npm auditYou can also inspect outdated packages:
npm outdatedDo not blindly update every dependency in a production application without testing.
A better workflow is:
Dependency Alert
|
v
Identify affected package
|
v
Read advisory
|
v
Update or apply recommended fix
|
v
Run tests
|
v
Deploy carefullyDependency security should be part of normal development rather than something you check only after an incident.
A Secure Request Flow
A useful way to think about API security is as a series of gates.
Incoming Request
|
v
HTTPS
|
v
Rate Limiting
|
v
Authentication
|
v
Authorization
|
v
Input Validation
|
v
Business Logic
|
v
Database
|
v
Safe ResponseNot every application will implement these steps in exactly this order.
The diagram is a mental model rather than a mandatory architecture.
The important idea is that security should be considered throughout the request lifecycle.
Example: Protecting an Admin Endpoint
Imagine your application has:
DELETE /api/products/:idOnly administrators should be able to delete products.
A simplified route could look like:
app.delete(
"/api/products/:id",
authenticate,
requireAdmin,
async (req, res) => {
const productId = Number(req.params.id);
if (!Number.isInteger(productId) || productId <= 0) {
return res.status(400).json({
error: "Invalid product ID"
});
}
const product = await findProduct(productId);
if (!product) {
return res.status(404).json({
error: "Product not found"
});
}
await deleteProduct(productId);
res.status(204).send();
}
);The request passes through several checks:
- Is the request authenticated?
- Is the authenticated user an administrator?
- Is the product ID valid?
- Does the product exist?
- Can the product safely be deleted?
That is much stronger than relying on the frontend to hide the delete button.
API Security vs Frontend Security
Consider a shopping application.
The frontend might display:
[ Delete Product ]only for administrators.
That is useful for the user interface.
But it is not sufficient security.
An attacker can bypass the interface entirely and send:
DELETE /api/products/15directly.
Therefore:
Frontend restrictions
+
Backend authorization
=
Better securityNever treat a hidden button, disabled input, or frontend role check as the final security control.
Authentication Example: Protecting a Route
A simplified authentication middleware might look like:
function authenticate(req, res, next) {
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(401).json({
error: "Authentication required"
});
}
// Token verification would happen here.
req.user = {
id: 42,
role: "user"
};
next();
}Then:
app.get("/api/profile", authenticate, (req, res) => {
res.json({
userId: req.user.id,
role: req.user.role
});
});This example intentionally leaves token verification out.
In a real application, use a well-tested authentication and session/token solution rather than creating cryptographic authentication logic yourself without understanding its security requirements.
Authentication Mistakes to Avoid
Avoid designing authentication around:
Plain-text passwords
Hardcoded credentials
Unlimited login attempts
Long-lived credentials without appropriate controls
Tokens exposed in URLs
Weak session handling
Home-made cryptography
Client-only authorizationPasswords should not be stored as plain text.
Use an established password hashing solution designed for password storage.
For important applications, consider established authentication providers or well-tested libraries rather than creating every part of authentication yourself.
Logging Security Events
Logging can help you understand suspicious activity.
Useful events can include:
- Repeated failed logins
- Authorization failures
- Unexpected administrative actions
- Suspicious request patterns
- Important configuration changes
- Server errors
For example:
console.warn("Authorization failure", {
userId: req.user?.id,
path: req.originalUrl,
method: req.method
});Be careful about what you log.
Do not casually log:
- Passwords
- Authentication tokens
- Session secrets
- API keys
- Sensitive personal information
Logs can become a security problem if they contain secrets.
Test the API Like an Attacker Would
Do not test only the happy path.
For an endpoint:
GET /api/users/:idtest:
Valid user ID
Missing user ID
Invalid user ID
Another user's ID
Unauthenticated request
Unauthorized request
Very large ID
Unexpected parameter valuesFor a login endpoint:
Correct credentials
Incorrect password
Unknown account
Repeated failed attempts
Missing fields
Malformed request body
Unexpected field typesThe goal is not to attack your own application recklessly.
The goal is to identify whether security rules actually hold when the request is manipulated.
A Simple API Security Test Matrix
| Test | Expected result |
|---|---|
| No authentication | 401 where authentication is required |
| Authenticated normal user accessing admin function | 403 |
| Invalid resource ID | 400 |
| Missing resource | 404 |
| Invalid request body | 400 |
| Excessive requests | Rate limiting response |
| Valid authorized request | Successful response |
| Unexpected server failure | Safe 500 response |
This makes security testing repeatable rather than relying on memory.
Common API Security Mistakes
Mistake 1: "The frontend is secure"
It is not enough.
The server must enforce security rules.
Mistake 2: "The user knows the ID, so they can access it"
Knowing an identifier does not automatically establish permission.
Mistake 3: "CORS protects my API"
CORS controls browser cross-origin behavior. It does not replace authentication or authorization.
Mistake 4: "I added JWT, so the API is secure"
Authentication tokens solve only part of the problem.
You still need authorization, validation, secure token handling, appropriate expiration/revocation strategy, and protection of sensitive endpoints.
Mistake 5: "The API is private because there is no frontend link"
Attackers can send HTTP requests without using your website's interface.
Mistake 6: "npm install means my dependencies are safe"
Dependencies can contain vulnerabilities.
Regularly review dependency advisories and update responsibly.
Mistake 7: "More error details are always better"
Detailed internal errors are useful for developers but can expose unnecessary information to clients.
Practical Node.js API Security Checklist
Before deploying a Node.js REST API, verify:
Authentication
- Sensitive endpoints require authentication
- Passwords are never stored as plain text
- Authentication failures are handled consistently
- Sessions or tokens are handled securely
- Login attempts are appropriately protected
Authorization
- Server-side authorization checks exist
- Users cannot access another user's resources without permission
- Admin endpoints require appropriate privileges
- Resource ownership is checked where necessary
- Authorization is not based only on frontend behavior
Input
- Request bodies are validated
- URL parameters are validated
- Query parameters are validated
- Unexpected input is handled safely
- Database queries use safe parameterization or equivalent mechanisms
Infrastructure
- HTTPS is enabled in production
- Secrets are stored outside source code
- Security headers are configured appropriately
- CORS is intentionally configured
- Rate limits exist where appropriate
- Dependencies are regularly reviewed
Monitoring
- Security-relevant events are logged
- Sensitive secrets are excluded from logs
- Errors do not expose unnecessary internal information
- Suspicious patterns can be investigated
What Should You Learn Next?
If you are learning backend development, do not try to master every security topic at once.
A practical progression is:
REST APIs
|
v
HTTP & Status Codes
|
v
Input Validation
|
v
Authentication
|
v
Authorization
|
v
Database Security
|
v
Rate Limiting
|
v
Logging & Monitoring
|
v
Deployment SecurityOnce these fundamentals are comfortable, study broader application-security concepts and security testing.
Karyvio Developer Takeaway
For a portfolio project, security features can make a simple application considerably more meaningful.
Instead of presenting:
"Created a Node.js CRUD application."
you can demonstrate:
"Built a REST API with authentication, role-based authorization, server-side validation, rate limiting, secure error handling, and protected resource access."
The important part is to implement these features correctly and explain why they exist.
A good portfolio project should make it possible for another developer to inspect the repository and understand:
- How users authenticate
- How permissions are enforced
- How input is validated
- How secrets are managed
- How errors are handled
- How suspicious activity is logged
- How dependencies are maintained
That demonstrates security awareness alongside backend development skills.
Final Takeaway
API security is not one package or one middleware function.
A safer REST API combines multiple layers:
Authentication
+
Authorization
+
Input Validation
+
Rate Limiting
+
Secure Configuration
+
Safe Error Handling
+
Logging
+
Dependency Management
+
Security TestingThe most important lesson is to treat every request as untrusted until the server has verified what the request is allowed to do.
Start with the basics, apply the controls consistently, and test the API directly rather than assuming that the frontend protects it.
For current security work, use established security guidance and well-maintained libraries rather than inventing authentication or cryptographic mechanisms yourself.







Comments (0)
Be the first to share your thoughts.