Skip to content
·10 min read

API Security for Vibe Coders Who Ship Without a Security Team

Rate limiting, input validation, and authentication patterns that protect your API routes from the most common attacks

Share

API rate limiting is the difference between an API that survives a bad day and one that goes down the moment someone writes a while(true) loop against your endpoints. 92% of US developers now use AI coding tools daily, and those tools generate API routes that accept anything, authenticate nothing, and have zero protection against abuse. Your AI built the happy path. Nobody built the defenses.

Veracode's 2025 research found that 45% of AI-generated code contains security vulnerabilities. API routes are where those vulnerabilities concentrate, because every API endpoint is a door into your application that anyone on the internet can knock on. Your frontend might look polished. Your API routes are probably wide open.

I have reviewed dozens of AI-generated API routes across Next.js and Express projects. The pattern is always the same. The route accepts a POST request, reads the body, does the thing, returns a response. No validation on the input. No check on who is calling. No limit on how often they can call. The AI built what you asked for and skipped everything you did not ask for.

Why AI Tools Skip API Security

AI coding tools optimize for "working code." You ask for an endpoint that creates a user, and you get an endpoint that creates a user. It works. You test it. It works again. You ship it.

The problem is that "works when I test it" and "works when 10,000 strangers hit it" are fundamentally different standards. AI tools do not add rate limiting because you did not ask for rate limiting. They do not validate input because your test data was always valid. They do not add authentication because your development environment did not need it.

This is not a flaw in AI tools. It is a gap in how we use them. Security is not a feature you add. It is a layer that wraps every feature. If you do not explicitly ask for it, you will not get it.

Rate Limiting Your API Routes

Rate limiting controls how many requests a client can make in a given time window. Without it, a single attacker (or a buggy client, or a bot) can overwhelm your server, drain your database connections, or rack up thousands of dollars in third-party API costs.

The Upstash Redis Approach (Production)

For production applications, use Upstash Redis with the @upstash/ratelimit package. It works at the edge, survives server restarts, and handles distributed deployments where multiple server instances need to share rate limit state.

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse } from "next/server";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "60 s"),
  analytics: true,
});

export async function POST(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: "Too many requests. Try again later." },
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": limit.toString(),
          "X-RateLimit-Remaining": remaining.toString(),
          "X-RateLimit-Reset": reset.toString(),
        },
      }
    );
  }

  // Your actual route logic here
}

This gives each IP address 10 requests per 60-second sliding window. The sliding window is important. A fixed window lets someone fire 10 requests at second 59 and 10 more at second 61. A sliding window prevents that burst.

The In-Memory Approach (Prototypes)

If you are prototyping and do not want to set up Redis, a simple in-memory Map works. It will reset when the server restarts and will not share state across multiple instances, but it is better than nothing.

const rateLimitMap = new Map<string, { count: number; resetTime: number }>();

function checkRateLimit(ip: string, maxRequests = 10, windowMs = 60000) {
  const now = Date.now();
  const record = rateLimitMap.get(ip);

  if (!record || now > record.resetTime) {
    rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs });
    return { allowed: true, remaining: maxRequests - 1 };
  }

  if (record.count >= maxRequests) {
    return { allowed: false, remaining: 0 };
  }

  record.count++;
  return { allowed: true, remaining: maxRequests - record.count };
}

Use the in-memory approach for development and early prototypes. Switch to Upstash before you have real users.

Key Takeaway

Rate limiting is the single cheapest security measure you can add. Ten lines of code prevent brute-force attacks, API abuse, and runaway costs. If you ship one API route today, add rate limiting to it. The Upstash free tier handles 10,000 requests per day, which covers most indie projects.

Input Validation With Zod

Every piece of data that enters your API through a request body, query parameter, or header is untrusted input. AI-generated routes typically read request.json() and pass whatever they get straight to a database query or external API call. This is how injection attacks happen.

Zod is the standard for runtime validation in TypeScript. Define a schema, parse the input, and handle the error if it does not match.

import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).trim(),
  role: z.enum(["user", "admin"]).default("user"),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const result = CreateUserSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      { error: "Invalid input", details: result.error.flatten() },
      { status: 400 }
    );
  }

  const { email, name, role } = result.data;
  // Now email, name, and role are guaranteed to be
  // the correct types with the constraints you defined
}

Always use safeParse (not parse, which throws), set maximum lengths, trim strings, and validate enum values against a whitelist. An attacker sending { "role": "superadmin" } should get a 400, not a new superadmin account.

EXPLAINER DIAGRAM: A horizontal flow showing three stages of API request processing. Stage 1 labeled RAW REQUEST shows a red box containing unvalidated JSON with fields like email, name, and role with question marks. An arrow points right to Stage 2 labeled ZOD VALIDATION showing a filter icon with green checkmarks next to valid fields (proper email format, name within length limits, role matching enum) and red X marks next to invalid fields (missing email, name exceeding 100 chars, role value not in enum). Another arrow points right to Stage 3 labeled SAFE DATA showing a green box with validated and typed data ready for database operations. Below the flow, a rejected path branches down from Stage 2 to a 400 BAD REQUEST response box.
Zod validates input at the boundary of your API. Invalid data never reaches your database or business logic.
New to App Security?

Start with the fundamentals before diving into API-specific patterns.

Read the basics

API Authentication Patterns

Rate limiting stops volume attacks. Validation stops malformed input. Authentication stops unauthorized access. You need all three.

API Keys for Service-to-Service

API keys are the simplest authentication method. The client includes a secret key in the request header, and your server checks it against a known value. Use this for internal services, webhooks, and admin endpoints.

export async function POST(request: NextRequest) {
  const apiKey = request.headers.get("x-api-key");

  if (!apiKey || apiKey !== process.env.BLOG_API_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Authenticated request, proceed
}

Never put API keys in query parameters (they appear in server logs and browser history). Always use headers. Store the key in environment variables, not in your codebase.

JWT Verification for User Sessions

If your app uses JWTs for user authentication, verify them on every API request. Do not just check that a token exists. Verify the signature, check the expiration, and validate the claims.

import { jwtVerify } from "jose";

async function verifyAuth(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;

  if (!token) return null;

  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(process.env.JWT_SECRET)
    );
    return payload;
  } catch {
    return null;
  }
}

The jose library works on Cloudflare Workers, Vercel Edge Functions, and Node.js. Avoid jsonwebtoken if you deploy to edge runtimes since it depends on Node.js crypto APIs that are not available everywhere.

Session Tokens With a Database

For apps that need instant revocation (admin panels, financial apps), store session tokens in a database instead of using JWTs. Generate a random token on login, store it with the user ID, and set it as an httpOnly cookie. Slower than JWT verification but gives you instant session invalidation.

Common Mistake

AI tools almost always store JWTs in localStorage. This exposes the token to any JavaScript running on your page, including third-party scripts and XSS attacks. Always store authentication tokens in httpOnly cookies. They are invisible to JavaScript and automatically sent with every request. If your AI-generated code uses localStorage.setItem("token", ...), move it to an httpOnly cookie immediately.

CORS Configuration for API Routes

Cross-Origin Resource Sharing (CORS) controls which domains can call your API from a browser. Without proper CORS configuration, any website on the internet can make requests to your API using your users' cookies.

export async function OPTIONS() {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "https://yourdomain.com",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
      "Access-Control-Max-Age": "86400",
    },
  });
}

Never set Access-Control-Allow-Origin: * on routes that use cookies or authentication. The wildcard means any site can call your API. For public, read-only endpoints (like a public product catalog), the wildcard is fine. For anything that writes data or requires authentication, specify your exact domain.

Common Attacks Your API Faces

Understanding the attacks helps you understand why each defense matters.

Brute force attacks try thousands of combinations per second. Rate limiting stops them.

Injection attacks embed malicious commands in form fields. Input validation with Zod rejects anything that does not match your schema.

Enumeration attacks probe your API to discover valid usernames or IDs. Always return the same error response whether a resource exists or not.

CSRF attacks trick authenticated users into making unwanted API calls. CORS configuration and anti-CSRF tokens stop them.

EXPLAINER DIAGRAM: A four-quadrant grid showing common API attacks and their defenses. Top-left quadrant labeled BRUTE FORCE shows a robot icon sending rapid requests with a shield icon labeled RATE LIMITING blocking them. Top-right quadrant labeled INJECTION shows a text input containing malicious SQL code with a shield labeled INPUT VALIDATION (ZOD) blocking it. Bottom-left quadrant labeled ENUMERATION shows a magnifying glass probing different endpoints with a shield labeled CONSISTENT ERROR RESPONSES blocking information leaks. Bottom-right quadrant labeled CSRF shows a malicious website trying to use a user's cookies with a shield labeled CORS PLUS CSRF TOKENS blocking the cross-origin request. Center text reads EVERY API ROUTE NEEDS ALL FOUR DEFENSES.
Each attack type targets a different weakness. Rate limiting, validation, authentication, and CORS work together as layers of defense.

Putting It All Together

Every protected API route should follow this order: rate limit first, authenticate second, validate third. The order matters because rate limiting is cheapest (no database call), authentication is next (one lookup), and validation is last (parse the body only after confirming the request is legitimate).

What This Means For You

Every API route you ship needs three things: a rate limit, input validation, and an authentication check. Start with your most sensitive endpoint (login or payments), then work outward.

  • If you are a founder: Add rate limiting to your login and payment endpoints today. It takes 15 minutes with Upstash and prevents the attacks that would destroy user trust in your MVP.
  • If you are changing careers: Understanding API security separates you from developers who only know how to build features. Add these patterns to your portfolio projects.
  • If you are a student: Learn Zod validation and rate limiting now. Every production app needs them, and most bootcamps skip them entirely.
Secure Your API Today

Start with the fundamentals of protecting your vibe-coded endpoints.

Get started
PJ
Pranay Joshi

20+ years building products at scale. VP of Product & Engineering, startup founder, and AI coach. Helping dreamers turn ideas into reality with vibe coding.

The Tuesday Shipping Report

Every Tuesday, one focused email:

  • - The tool or technique that's actually working right now
  • - A real problem from the community (and how to solve it)
  • - What changed this week in the vibe coding landscape

Read by 1,000+ founders, developers, and creators building with AI. Free forever. No spam.