◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-03-20Advanced

Building Secure API Backends with Antigravity Agents — Authentication, Validation & Rate Limiting Patterns

An advanced guide to designing and implementing secure API backends with JWT authentication, input validation, and rate limiting using Antigravity's AI agents.

Antigravity385API6security22authentication10JWTvalidation5rate limitingbackend3

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:

  1. Layer 1 — Rate Limiting: Block excessive requests at the gateway level
  2. Layer 2 — Authentication & Authorization: Verify user identity and permissions via JWT
  3. 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 responses

Step-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/min

Pattern 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 issues

Pattern 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: HS256 is lightweight, but for microservice architectures, consider RS256 (asymmetric keys) so each service can verify tokens without holding the private key.
  • Rate limit storage: A Map works 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.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

▣ App Dev2026-10-15
I Swapped an Immutable Asset at the Same URL, and Only the Browser Kept the Old Copy
Overwrite a long-cached JS file at the same URL and neither a CDN purge nor a refresh will make the browser fetch it again. Four delivery styles — same URL, no-cache, query string, content-hash filename — compared in Chromium, plus a complete build-step script that adds the hash.
▣ App Dev2026-09-29
Purging Cloudflare Didn't Bring Back the New Wallpapers: the Stale JPEGs Were Held by XPageSpeed on the Origin
An operations record from a wallpaper app: images overwritten at the same URL kept arriving stale even after a Cloudflare purge. Covers the three-way local/origin/edge probe that names the guilty layer, when prefix purge beats per-URL purge, and how to switch off Xserver's XPageSpeed in .htaccess without a 500.
▣ App Dev2026-09-19
I Asked for the Whole Quote PDF Tool at Once, Then Split It Into Four Requests
How I moved my client quote paperwork from copy-and-edit to a small PDF tool built with Antigravity, why the first single-shot request had to be thrown away, and the four smaller requests that replaced it, with working code.