API Security and Performance Optimization
Secure the REST API using security headers, rate limiting, input controls, password protection, MongoDB indexes, and efficient database queries.
Understand Common REST API Security Risks
A public REST API can be exposed to several security threats.
Common risks include:
- Broken authentication.
- Broken object-level authorization.
- Broken function-level authorization.
- Injection attacks.
- Cross-site scripting in consuming applications.
- Excessive request rates.
- Sensitive data exposure.
- Mass assignment vulnerabilities.
- Unrestricted resource consumption.
Security should be implemented at multiple layers rather than relying on one middleware package.
Our application will combine authentication, authorization, validation, request limits, secure response handling, and database access restrictions.
API security requires multiple layers of protection.
Authentication and authorization failures are common API risks.
Never trust arbitrary client input.
Configure Helmet Security Headers
Helmet is an Express middleware package that sets HTTP headers to help protect applications against common web vulnerabilities.
It configures headers such as:
- Content-Security-Policy.
- X-Content-Type-Options.
- Referrer-Policy.
- Strict-Transport-Security under appropriate conditions.
Helmet is already installed in our project.
Register it near the beginning of Express application initialization.
Review the default configuration and customize it according to your deployment requirements.
import helmet from 'helmet';
app.use(helmet());Registers Helmet's default security header middleware.
Helmet sets security-related HTTP headers.
Register security middleware early.
Review security headers according to application requirements.
Implement API Rate Limiting
Rate limiting restricts the number of requests a client can make during a specified time window.
For example, a login endpoint might allow only a limited number of attempts from a particular IP address within a time window.
We will use express-rate-limit.
Rate limiting helps reduce brute-force attempts and excessive resource consumption.
For distributed deployments, configure a shared rate-limit store rather than relying on independent in-memory counters.
The default in-memory rate-limit store is process-specific. Use a shared store such as Redis when requests are distributed across multiple application instances.
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 100,
standardHeaders: 'draft-8',
legacyHeaders: false,
message: {
success: false,
message: 'Too many requests. Please try again later.'
}
});
app.use('/api', apiLimiter);Limits clients to 100 requests within a 15-minute window using the default in-memory store.
Rate limiting controls excessive requests.
Login endpoints may require stricter limits.
Use a shared store for distributed applications.
Prevent Mass Assignment Vulnerabilities
Mass assignment occurs when an API blindly accepts client-provided properties and applies them directly to a database document.
Imagine a user update endpoint that accepts:
{
"name": "John",
"role": "admin",
"isActive": false
}
If the backend directly passes this object into a database update operation, an ordinary user may be able to modify privileged properties.
Prevent this by explicitly selecting the properties that clients are allowed to update.
For profile updates, allow only fields such as name and age.
Role changes should be implemented through a separate administrator-authorized operation.
Do not spread arbitrary request bodies into Mongoose updates. Restrict writable properties using validation schemas and explicit allowlists.
export function pickUserUpdateFields(body) {
const allowedFields = ['name', 'age'];
return Object.fromEntries(
Object.entries(body).filter(([key]) =>
allowedFields.includes(key)
)
);
}Creates an update object containing only explicitly permitted properties.
Mass assignment can expose privileged fields.
Use explicit property allowlists.
Protect role and account-status modifications.
Understand MongoDB Indexes
Indexes improve the efficiency of database queries by allowing MongoDB to locate matching documents without scanning the entire collection.
Without an appropriate index, a query may perform a collection scan.
For example, our application frequently searches users by email.
An index on email can improve this lookup.
MongoDB supports several index types:
- Single-field indexes.
- Compound indexes.
- Multikey indexes.
- Text indexes.
- Geospatial indexes.
- TTL indexes.
The correct index depends on the query patterns used by the application.
Common Index Types
Indexes one field
Indexes multiple fields
Indexes array values
Supports text search
Automatically expires documents
Indexes improve query efficiency.
Choose indexes based on actual query patterns.
Indexes consume storage and add write overhead.
Create a Unique Email Index
Our user model declares email as unique.
For clarity, you can explicitly declare the index on the schema.
A unique index ensures that MongoDB does not allow multiple documents with the same indexed email value.
Existing duplicate records must be resolved before MongoDB can successfully create the index.
In production, manage index creation through controlled database migrations or deployment procedures. Do not rely on automatic index creation for every application startup.
userSchema.index(
{ email: 1 },
{ unique: true }
);Creates a unique ascending index on the email field.
Unique indexes enforce uniqueness at the database level.
Resolve duplicate records before index creation.
Manage production indexes deliberately.
Analyze MongoDB Query Performance
MongoDB provides explain() to inspect how a query executes.
It can reveal:
- Whether MongoDB uses an index.
- Whether the query performs a collection scan.
- How many documents were examined.
- How many documents were returned.
- Query execution statistics.
For example, a query filtering users by email can be inspected using execution statistics.
const result = await User.find({
email: 'john@example.com'
}).explain('executionStats');
console.log(result.executionStats);Returns execution statistics for the email lookup query.
explain() helps diagnose query execution.
Inspect documents examined and returned.
Use query plans to identify missing indexes and inefficient queries.
Prevent MongoDB Injection
MongoDB injection occurs when untrusted input influences database query operators or query structure.
For example, an application should never accept arbitrary MongoDB filters directly from the client.
Avoid patterns such as:
User.find(req.query);
A malicious client might provide unexpected operator structures.
Instead:
- Validate query parameters.
- Allowlist supported filter fields.
- Convert values to expected data types.
- Avoid blindly spreading request objects into database operations.
- Use controlled query builders.
const filter = {};
if (typeof req.query.email === 'string') {
filter.email = req.query.email.toLowerCase().trim();
}
if (typeof req.query.isActive === 'string') {
if (req.query.isActive === 'true') {
filter.isActive = true;
} else if (req.query.isActive === 'false') {
filter.isActive = false;
}
}
const users = await User.find(filter).select('-password');Constructs a query from explicitly supported scalar parameters instead of passing arbitrary client data to MongoDB.
Never accept arbitrary MongoDB query objects from clients.
Allowlist query fields and operators.
Validate types before database operations.