Setup and context
API backend security is one of the most critical aspects of any production application. Missing authentication, insufficient input validation, and absent rate limiting can lead to data breaches, denial of service, and significant business impact.
This guide walks you through building a production-ready secure API backend using Antigravity's AI agents. You'll implement JWT authentication, schema validation with Zod, and token bucket rate limiting — all orchestrated through Antigravity's agentic workflow to dramatically accelerate development while maintaining security best practices.
Target audience: Intermediate to advanced developers familiar with Antigravity basics and experienced with Node.js/TypeScript API development.
Prerequisites & Environment Setup
Requirements
- Antigravity 1.20 or later
- Node.js 20+
- TypeScript 5.x
- npm or pnpm
Project Initialization
Ask Antigravity's agent to scaffold the project structure with a prompt like this:
# Prompt for Antigravity agent
"Create an Express + TypeScript API server project.
Include eslint, prettier, and vitest.
Set up src/routes, src/middleware, and src/validators directories."The agent will generate a project structure like:
project-root/
├── src/
│ ├── index.ts # Entry point
│ ├── routes/
│ │ ├── auth.ts # Auth routes
│ │ └── users.ts # User routes
│ ├── middleware/
│ │ ├── authenticate.ts # JWT auth middleware
│ │ ├── validate.ts # Validation middleware
│ │ └── rateLimit.ts # Rate limiting middleware
│ └── validators/
│ └── schemas.ts # Zod schema definitions
├── tsconfig.json
└── package.json
Architecture & Design Philosophy
A secure API backend follows a three-layer defense model:
- Layer 1 — Rate Limiting: Block excessive requests at the gateway level
- Layer 2 — Authentication & Authorization: Verify user identity and permissions via JWT
- Layer 3 — Input Validation: Strictly validate request bodies and parameters
The ordering matters. Placing rate limiting first reduces load on authentication processing and prevents brute-force attacks before they reach your business logic.
Document this design in your AGENTS.md so Antigravity agents maintain consistent security patterns across all generated code:
# AGENTS.md (Security Section)
## API Security Policy
- Apply rate limiting to all endpoints
- Require JWT middleware on all authenticated routes
- Validate request bodies with Zod schemas before business logic
- Never expose internal details in error responsesStep-by-Step Implementation
Step 1: JWT Authentication Middleware
Implement a middleware that issues and verifies JWT tokens:
// src/middleware/authenticate.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
interface TokenPayload {
userId: string;
role: "user" | "admin";
iat: number;
exp: number;
}
// Access token verification middleware
export function authenticate(
req: Request,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.status(401).json({
error: "UNAUTHORIZED",
message: "Authentication token required",
});
return;
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as TokenPayload;
// Attach user info to request object
req.user = { userId: payload.userId, role: payload.role };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({
error: "TOKEN_EXPIRED",
message: "Token has expired",
});
return;
}
res.status(401).json({
error: "INVALID_TOKEN",
message: "Invalid token",
});
}
}
// Role-based authorization middleware
export function authorize(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
res.status(403).json({
error: "FORBIDDEN",
message: "Insufficient permissions for this operation",
});
return;
}
next();
};
}Antigravity agent prompt tip: Ask "Add refresh token rotation with Redis storage" and the agent will generate the full token rotation logic automatically.
Step 2: Schema Validation with Zod
Define type-safe, declarative validation schemas using Zod:
// src/validators/schemas.ts
import { z } from "zod";
// User registration schema
export const registerSchema = z.object({
body: z.object({
email: z
.string()
.email("Please enter a valid email address")
.max(255),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.max(128)
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Must contain at least one uppercase letter, one lowercase letter, and one number"
),
name: z.string().min(1).max(100).trim(),
}),
});
// Pagination schema for query parameters
export const paginationSchema = z.object({
query: z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(["createdAt", "updatedAt", "name"]).default("createdAt"),
order: z.enum(["asc", "desc"]).default("desc"),
}),
});
// Generic validation middleware
// src/middleware/validate.ts
import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
export function validate(schema: AnyZodObject) {
return async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (err) {
if (err instanceof ZodError) {
const errors = err.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
res.status(400).json({
error: "VALIDATION_ERROR",
details: errors,
});
return;
}
next(err);
}
};
}Step 3: Token Bucket Rate Limiting
Implement per-IP rate limiting using the token bucket algorithm. Here's both an in-memory version and guidance for Redis-backed production use:
// src/middleware/rateLimit.ts
import { Request, Response, NextFunction } from "express";
interface BucketEntry {
tokens: number;
lastRefill: number;
}
// In-memory token bucket (for dev and small-scale deployments)
class TokenBucket {
private buckets = new Map<string, BucketEntry>();
private maxTokens: number;
private refillRate: number; // tokens per second
constructor(maxTokens: number, refillRate: number) {
this.maxTokens = maxTokens;
this.refillRate = refillRate;
// Periodically clean up stale entries
setInterval(() => this.cleanup(), 60_000);
}
consume(key: string): { allowed: boolean; remaining: number; retryAfter?: number } {
const now = Date.now();
let bucket = this.buckets.get(key);
if (!bucket) {
bucket = { tokens: this.maxTokens - 1, lastRefill: now };
this.buckets.set(key, bucket);
return { allowed: true, remaining: bucket.tokens };
}
// Refill tokens
const elapsed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(
this.maxTokens,
bucket.tokens + elapsed * this.refillRate
);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
const retryAfter = Math.ceil((1 - bucket.tokens) / this.refillRate);
return { allowed: false, remaining: 0, retryAfter };
}
bucket.tokens -= 1;
return { allowed: true, remaining: Math.floor(bucket.tokens) };
}
private cleanup(): void {
const cutoff = Date.now() - 300_000; // Remove entries older than 5 min
for (const [key, entry] of this.buckets) {
if (entry.lastRefill < cutoff) this.buckets.delete(key);
}
}
}
// Create separate buckets per endpoint type
const generalBucket = new TokenBucket(60, 1); // 60 req/min
const authBucket = new TokenBucket(10, 0.167); // 10 req/min (brute-force prevention)
export function rateLimit(type: "general" | "auth" = "general") {
const bucket = type === "auth" ? authBucket : generalBucket;
return (req: Request, res: Response, next: NextFunction): void => {
const key = req.ip || req.socket.remoteAddress || "unknown";
const result = bucket.consume(key);
res.setHeader("X-RateLimit-Remaining", result.remaining.toString());
if (!result.allowed) {
res.setHeader("Retry-After", result.retryAfter!.toString());
res.status(429).json({
error: "RATE_LIMITED",
message: "Too many requests. Please try again later.",
retryAfter: result.retryAfter,
});
return;
}
next();
};
}Step 4: Wiring It All Together
Combine the three middleware layers into your routes to achieve the full defense-in-depth pattern:
// src/routes/users.ts
import { Router } from "express";
import { authenticate, authorize } from "../middleware/authenticate";
import { validate } from "../middleware/validate";
import { rateLimit } from "../middleware/rateLimit";
import { paginationSchema } from "../validators/schemas";
const router = Router();
// GET /users — admin only, rate limited
router.get(
"/",
rateLimit("general"), // Layer 1: Rate limiting
authenticate, // Layer 2: Authentication
authorize("admin"), // Layer 2: Authorization
validate(paginationSchema), // Layer 3: Validation
async (req, res) => {
const { page, limit, sort, order } = req.query as any;
// Business logic — DB queries, etc.
res.json({ users: [], total: 0, page, limit });
}
);
export default router;Advanced Patterns
Pattern 1: Endpoint-Specific Rate Limit Strategies
Apply strict limits to authentication endpoints (login, password reset) and more permissive limits to public-facing APIs:
// Strict limits for auth routes
app.use("/api/auth", rateLimit("auth")); // 10 req/min
// Standard limits for general API
app.use("/api/v1", rateLimit("general")); // 60 req/minPattern 2: Security Audit Agent via Manager Surface
Leverage Antigravity's Manager Surface to configure a dedicated security auditor agent:
# AGENTS.md — Security Audit Agent
## security-auditor
You are a security-focused code reviewer. Check for:
- SQL injection and XSS vulnerabilities
- Authentication bypass possibilities
- Hardcoded environment variables
- Internal information leaks in error messages
- CORS configuration issuesPattern 3: Helmet and CORS Configuration
Automate security header configuration with Helmet:
import helmet from "helmet";
import cors from "cors";
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || [],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
}));Troubleshooting
JWT tokens always rejected as invalid
The most common cause is a missing JWT_SECRET environment variable. Verify your .env file exists and dotenv is loaded before any middleware runs. Also confirm you're using the same secret for both token issuance and verification.
Rate limiting not working (keeps resetting)
The in-memory token bucket resets on server restart. During development with nodemon, frequent restarts are expected. For production, switch to a Redis-backed implementation. Ask Antigravity's agent: "Switch rate limiting to Redis with ioredis".
Zod validation errors too verbose for production
In production, returning detailed validation messages can be a security risk. Control output verbosity based on environment:
const isDev = process.env.NODE_ENV === "development";
// In production, return only field names without detailed messages
const errors = err.errors.map((e) => ({
field: e.path.join("."),
...(isDev && { message: e.message }),
}));Performance Considerations
- JWT verification cost:
HS256is lightweight, but for microservice architectures, considerRS256(asymmetric keys) so each service can verify tokens without holding the private key. - Rate limit storage: A
Mapworks for single-process deployments, but multi-process or multi-server setups require Redis. Tell Antigravity's agent"Switch to Redis-based rate limiting"for an automatic migration. - Zod performance: For extremely high-throughput APIs, Ajv (JSON Schema-based) can be faster since it compiles validators ahead of time. For typical web APIs, Zod provides more than adequate performance with superior TypeScript integration.
Conclusion
Antigravity's AI agents can dramatically accelerate secure API backend development while maintaining best practices. The three-layer defense model covered in this guide — rate limiting, JWT authentication, and Zod validation — provides production-grade security when properly implemented.
The key takeaway: never blindly trust agent-generated code for security-critical paths. Define your security policies in AGENTS.md, set up a security audit agent via Manager Surface, and always conduct human review of authentication and authorization logic.
For next steps, check out Building Edge APIs with Antigravity + Cloudflare Workers and Designing Database Layers with Antigravity + Prisma ORM. You can also leverage Antigravity's AI Code Review to automate security audits in your workflow.