API Security Best Practices for AI-Built Applications
Secure your API endpoints. Fix missing authentication, open CORS, rate limiting gaps, and input validation issues in AI-generated backends.
By Daniel A · Kraftwire Software
· 9 min readKey Takeaway
API security is the most overlooked part of web application development. Your API endpoints are publicly accessible attack surfaces, and protecting them requires authentication, validation, rate limiting, and proper error handling at every level.
Why API Security Matters More Than Ever
Modern web applications are built on APIs. Your frontend talks to your backend through API calls. Third-party integrations connect through APIs. Mobile apps, webhooks, and partner systems all communicate through your API layer.
Every API endpoint is a potential entry point for attackers. Unlike a website where users interact through a controlled UI, API consumers can send any request they want. They are not limited to what your frontend allows. They can modify headers, change payloads, and call endpoints in sequences you never intended.
Authentication: The First Line of Defense
Every API endpoint that handles sensitive data needs authentication. This sounds obvious, but it is the most common gap we find in security scans.
Token-Based Authentication
Most modern APIs use token-based authentication. The client includes a bearer token in the Authorization header, and the server validates it before processing the request.
// Validating a JWT token in an API handler
import { verify } from "jsonwebtoken";
async function authenticateRequest(req: Request) {
const authHeader = req.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
throw new Error("Missing authentication token");
}
const token = authHeader.slice(7);
try {
const payload = verify(token, process.env.JWT_SECRET);
return payload;
} catch {
throw new Error("Invalid or expired token");
}
}
API Key Authentication
For server-to-server communication, API keys are common. But API keys are not the same as user authentication. They identify the calling application, not the user.
**Best practices for API keys:**
Generate cryptographically random keys (at least 32 bytes)
Hash keys before storing them in your database
Support key rotation without downtime
Set expiration dates on keys
Log all key usage for auditing
Input Validation
Never trust data that comes from API requests. Every field in every request body, query parameter, and header should be validated before processing.
Why Client-Side Validation Is Not Enough
Your frontend might validate that an email field contains a valid email address. But anyone can call your API directly with curl, Postman, or a custom script. Client-side validation is a user experience feature, not a security measure.
Schema Validation
Use a schema validation library to define the expected shape of every API input. This catches type mismatches, missing fields, and values outside expected ranges.
import { z } from "zod";
const createUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
role: z.enum(["user", "editor"]),
});
export async function handleCreateUser(req: Request) {
const body = await req.json();
const parsed = createUserSchema.safeParse(body);
if (!parsed.success) {
return new Response(
JSON.stringify({ error: "Invalid input", details: parsed.error.issues }),
{ status: 400 }
);
}
// Process with validated data
const { email, name, role } = parsed.data;
}
SQL Injection Prevention
If your API builds database queries from user input, SQL injection is a real risk. Always use parameterized queries or an ORM that handles parameterization automatically.
// Dangerous: string concatenation
const query = "SELECT * FROM users WHERE id = '" + userId + "'";
// Safe: parameterized query
const query = "SELECT * FROM users WHERE id = $1";
const result = await db.query(query, [userId]);
Rate Limiting
Without rate limiting, an attacker can flood your API with requests. This can crash your server, exhaust your database connections, or run up your cloud bill.
Rate Limiting Strategies
**Fixed Window**: Count requests per time window (e.g., 100 requests per minute). Simple to implement but can allow bursts at window boundaries.
**Sliding Window**: Track requests over a rolling time period. Smoother than fixed window but slightly more complex.
**Token Bucket**: Each client gets a bucket of tokens that refill at a fixed rate. Allows short bursts while enforcing average limits.
Different Limits for Different Endpoints
Not all endpoints need the same limits. Authentication endpoints should have strict limits (5-10 attempts per 15 minutes) to prevent brute force attacks. Read endpoints can be more generous. Write endpoints should be moderate.
Authorization: Beyond Authentication
Authentication tells you who the user is. Authorization determines what they can do. These are separate concerns, and conflating them is a common security mistake.
Object-Level Authorization
Every time your API returns or modifies a specific resource, verify that the authenticated user has permission to access that resource. This prevents Insecure Direct Object Reference (IDOR) vulnerabilities.
// Bad: Only checks authentication
app.get("/api/invoices/:id", async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
return res.json(invoice);
});
// Good: Checks both authentication and authorization
app.get("/api/invoices/:id", async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
if (invoice.userId !== req.user.id) {
return res.status(403).json({ error: "Forbidden" });
}
return res.json(invoice);
});
Error Handling
API error responses should be helpful for legitimate users and unhelpful for attackers.
What Not to Include in Error Responses
Stack traces
Database query details
Internal file paths
Server version information
Specific reasons why authentication failed (e.g., "password incorrect" vs "invalid credentials")
Consistent Error Format
Use a consistent error response format across your entire API. This makes it easier for clients to handle errors and harder for attackers to fingerprint your technology stack.
// Consistent error response
function apiError(status: number, message: string) {
return new Response(
JSON.stringify({ error: message, status }),
{ status, headers: { "Content-Type": "application/json" } }
);
}
CORS Configuration
Cross-Origin Resource Sharing controls which domains can call your API from the browser. A misconfigured CORS policy can allow any website to make requests on behalf of your users.
Production CORS Settings
Specify exact allowed origins (never use wildcard in production)
Limit allowed methods to what your API actually uses
Restrict allowed headers
Set appropriate max-age for preflight caching
Security Headers
Your API responses should include security headers that protect against common attack vectors.
X-Content-Type-Options: nosniff (prevents MIME type sniffing)
X-Frame-Options: DENY (prevents clickjacking)
Strict-Transport-Security (enforces HTTPS)
Cache-Control: no-store (for sensitive data endpoints)
Logging and Monitoring
Log every API request with enough detail to investigate incidents, but never log sensitive data like passwords, tokens, or personal information.
What to Log
Timestamp and request ID
HTTP method, path, and status code
Authenticated user ID
Client IP address
Response time
Rate limit status
Your API Security Checklist
Authentication required on all sensitive endpoints
Input validation on every request using schema validation
Parameterized database queries (no string concatenation)
Rate limiting with appropriate limits per endpoint type
Object-level authorization checks on resource access
Generic error messages without internal details
CORS restricted to specific domains
Security headers on all responses
Request logging without sensitive data
API key rotation support and expiration
Scan Your API Today
SimplyScan checks your API endpoints for exposed secrets, missing authentication, CORS misconfigurations, and security header gaps. Run a scan to find the vulnerabilities in your API before attackers do.