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.

Antigravity338API6security16authentication9JWTvalidation4rate 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-05-04
Building a Veo 3 Video Generation App with Antigravity — From API to Launch
A hands-on guide to building a video generation iOS app using the Veo 3 API with Antigravity. Covers async polling architecture, SwiftUI implementation, and monetization design.
App Dev2026-07-15
Quarantining the Dependencies Your Agent Adds, Before They Install
When an agent adds a dependency overnight, nobody reviews the lifecycle scripts that run at install time. Here is how I turned the default off and built a quarantine score to let the safe ones through.
App Dev2026-07-14
Protecting Screenshot Tests for AdMob Screens from Ad Nondeterminism
Screens with ads turn red on every screenshot run, and eventually nobody reviews the diffs. Here is a design that seals off AdMob banner nondeterminism and leaves only real layout breaks in your checks, with Compose code and Antigravity-driven diff triage.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →