Vibe Code a REST API Without Leaving It Wide Open

Build an API with AI — validation, auth, rate limiting, and pagination. The four things it skips because none of them stop the API working.

C
CodeIllusion Team
#vibe-coding #api #backend #tutorial #security
Vibe Code a REST API Without Leaving It Wide Open

APIs are easy to generate and easy to leave open. Every gap below produces an API that responds correctly to every request you test — which is exactly why they survive to production.

What You’re Building

A REST API for the projects data from the CRUD tutorial:

GET    /api/projects       list (paginated)
POST   /api/projects       create
GET    /api/projects/:id   read one
PATCH  /api/projects/:id   update
DELETE /api/projects/:id   delete

The First Prompt

Build REST endpoints for projects using Next.js App Router route handlers and TypeScript with Supabase.

Authenticate every request from the session server-side. Derive the user ID from the session only — never accept it from the request body or query.

Validate all input with Zod: title required max 200 chars, description max 2000, status one of active/done/archived, due_date optional ISO date. Reject unknown fields.

Every query filters by both record ID and authenticated user ID. Return proper status codes: 200, 201, 400, 401, 404, 422. Consistent JSON error shape.

The identity clause is the important one. Without it you’ll get something like:

const { userId, title } = await req.json()          // WRONG
await supabase.from('projects').insert({ user_id: userId, title })

An endpoint where the caller declares who they are. Anyone can pass any ID.

Pagination — Before You Need It

An unbounded list endpoint returns every row. With 100 records that’s invisible. With 100,000 it takes your database down.

Paginate GET /api/projects with cursor-based pagination. Default limit 25, maximum 100 — clamp anything higher rather than erroring. Return { data, nextCursor, hasMore }. Use keyset pagination on created_at and id, not offset.

Offset pagination degrades badly on large tables because the database still walks every skipped row. Keyset stays fast regardless of depth.

Rate Limiting

The gap that turns into a bill.

Add rate limiting with Upstash Ratelimit backed by Redis. 100 requests per minute per authenticated user on read endpoints, 20 per minute on writes. Return 429 with a Retry-After header. Apply it in middleware so no route can be added without it.

Middleware matters — per-route limiting means the next endpoint you add is unprotected by default.

If any endpoint calls a paid API — a language model, email, SMS — it needs a hard per-user quota on top:

Any endpoint calling a paid external API must require authentication and enforce a daily per-user cap stored in the database. Return 402 when exceeded.

Errors That Don’t Leak

Default error handling returns stack traces, file paths, and sometimes connection strings.

In production return generic error messages with a stable error code and no internal detail. Log the full error server-side with a request ID, and include that ID in the response so I can correlate. Never return stack traces or raw database errors.

A response of {"error":"duplicate key value violates unique constraint \"projects_slug_key\""} tells an attacker your schema. {"error":"CONFLICT","requestId":"req_a91f"} doesn’t.

CORS

If only your own frontend calls this API, lock it down. AI defaults to permissive settings because permissive settings never cause errors.

Restrict CORS to my production domain and localhost for development. Do not use a wildcard origin. Only allow the methods and headers actually needed.

Test It Like an Attacker

Five minutes, and it finds most of what’s wrong:

# 1. No auth — should be 401
curl -i localhost:3000/api/projects

# 2. Someone else's record — should be 404
curl -i localhost:3000/api/projects/OTHER_USER_RECORD_ID -H "Cookie: $MY_SESSION"

# 3. Oversized payload — should be 422
curl -i -X POST localhost:3000/api/projects \
  -H "Content-Type: application/json" -H "Cookie: $MY_SESSION" \
  -d "{\"title\":\"$(python3 -c 'print("x"*10000)')\"}"

# 4. Injected identity — should be ignored
curl -i -X POST localhost:3000/api/projects \
  -H "Content-Type: application/json" -H "Cookie: $MY_SESSION" \
  -d '{"title":"test","user_id":"SOMEONE_ELSES_ID"}'

# 5. Rate limit — should hit 429
for i in $(seq 1 200); do curl -s -o /dev/null -w "%{http_code} " \
  localhost:3000/api/projects -H "Cookie: $MY_SESSION"; done

Test 4 is the one people are surprised by. If the record gets created under the injected ID, your API lets anyone write data as anyone.

Documenting It

If anyone else will use this:

Generate an OpenAPI 3.1 spec from these route handlers, derived from the Zod schemas so it stays accurate. Serve Scalar docs at /api/docs.

Generating from the schemas rather than hand-writing means the docs can’t drift.

The Checklist

  • Identity from session/token only, never from request body
  • Zod validation on every input, unknown fields rejected
  • Max lengths on all string fields
  • Every query filters by authenticated user
  • Pagination with a clamped maximum
  • Rate limiting applied in middleware
  • Hard quotas on any paid-API endpoint
  • Generic production errors with correlation IDs
  • CORS restricted to known origins
  • All five curl tests behave correctly

The Pattern

Every item on that list shares a property: the API works perfectly without it. That’s what makes APIs the most dangerous thing to vibe code without review — there’s no visual output to tell you something’s wrong.

Test it like someone trying to break it, because eventually someone will.

Next: Vibe code an admin panel adds role-based access on top of this, and vibe coding security covers the full picture.

Related reading:

Frequently Asked Questions

Can AI build a production REST API? +

It builds working endpoints reliably. What it omits is everything that doesn't affect whether requests succeed: input validation, rate limiting, pagination limits, and consistent error handling. Those need to be requested by name.

How should an AI-built API handle authentication? +

Session cookies for your own frontend, API keys or bearer tokens for third-party consumers. Never accept a user ID from the request body to identify the caller — derive identity from the verified token or session on every request.

What's the most common security hole in AI-generated APIs? +

Trusting client input for identity or authorization. An endpoint that accepts userId in the body and returns that user's data lets anyone read anything. Identity must come from the auth layer, never from the request.

Do I need rate limiting on my API? +

Yes, on every public endpoint. Without it a single script can exhaust your database connections or run up costs on any paid service you call. It's a few lines with Upstash Ratelimit and it prevents the most expensive class of incident.

Tagged:

#vibe-coding #api #backend #tutorial #security

Enjoyed this article?

Get more AI tool picks, coding tutorials, and no-code automation guides every week. No spam, ever.

Found this useful? Share it:

More in AI Coding Tools