# How to Build API backend with V0

- Tool: v0
- Difficulty: Beginner
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

Build a production-ready REST API backend with V0 using Next.js API routes, Supabase for data, API key authentication, and rate limiting. You'll get a developer dashboard for managing keys, viewing request logs, and testing endpoints — all in about 30-60 minutes without touching a terminal.

## Before you start

- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Basic understanding of REST APIs (GET, POST, JSON responses)
- A use case for your API (e.g., products, users, or orders data)

## Step-by-step guide

### 1. Set up the project and database schema with V0

Open V0 and start a new project. Use the Connect panel to add Supabase — this auto-provisions your database URL and keys. Then prompt V0 to create the schema for API key management and request logging.

```
// Paste this prompt into V0's AI chat:
// Build a REST API management system. Create a Supabase schema with these tables:
// 1. api_keys: id (uuid PK), user_id (uuid FK to auth.users), key (text unique), name (text), created_at (timestamptz), last_used_at (timestamptz), is_active (boolean default true)
// 2. request_logs: id (uuid PK), api_key_id (uuid FK), method (text), path (text), status_code (int), response_time_ms (int), created_at (timestamptz)
// 3. rate_limits: id (uuid PK), api_key_id (uuid FK), window_start (timestamptz), request_count (int default 1)
// Add RLS policies so users can only see their own API keys.
// Generate the SQL migration and TypeScript types.
```

> Pro tip: Use V0's prompt queuing — queue up to 10 prompts while the first one generates. Queue the schema prompt first, then the middleware prompt next.

**Expected result:** Supabase is connected via the Connect panel, tables are created, and TypeScript types are generated for api_keys, request_logs, and rate_limits.

### 2. Create the API key validation middleware

Prompt V0 to generate a shared middleware module that validates API keys from the Authorization header, checks rate limits, and logs each request. This module will be imported by every API route handler.

```
import { createClient } from '@supabase/supabase-js'
import { NextRequest, NextResponse } from 'next/server'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function validateApiKey(req: NextRequest) {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader?.startsWith('Bearer ')) {
    return { error: 'Missing API key', status: 401 }
  }

  const key = authHeader.slice(7)
  const { data: apiKey } = await supabase
    .from('api_keys')
    .select('id, user_id, is_active')
    .eq('key', key)
    .eq('is_active', true)
    .single()

  if (!apiKey) {
    return { error: 'Invalid API key', status: 401 }
  }

  const windowStart = new Date(Date.now() - 60000).toISOString()
  const { count } = await supabase
    .from('request_logs')
    .select('*', { count: 'exact', head: true })
    .eq('api_key_id', apiKey.id)
    .gte('created_at', windowStart)

  if ((count ?? 0) >= 60) {
    return { error: 'Rate limit exceeded', status: 429 }
  }

  return { apiKey }
}
```

**Expected result:** The middleware module is created. It exports a validateApiKey function that returns either the API key object or an error response.

### 3. Build the REST API route handlers

Prompt V0 to generate CRUD route handlers for your resource. Each handler imports the middleware, validates the API key, processes the request, and logs the result. Use V0's prompt queuing to generate multiple resource endpoints at once.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { validateApiKey } from '@/lib/api-middleware'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function GET(req: NextRequest) {
  const start = Date.now()
  const auth = await validateApiKey(req)
  if ('error' in auth) {
    return NextResponse.json({ error: auth.error }, { status: auth.status })
  }

  const { searchParams } = new URL(req.url)
  const page = parseInt(searchParams.get('page') ?? '1')
  const limit = Math.min(parseInt(searchParams.get('limit') ?? '20'), 100)

  const { data, count, error } = await supabase
    .from('items')
    .select('*', { count: 'exact' })
    .range((page - 1) * limit, page * limit - 1)

  await supabase.from('request_logs').insert({
    api_key_id: auth.apiKey.id,
    method: 'GET',
    path: '/api/v1/items',
    status_code: error ? 500 : 200,
    response_time_ms: Date.now() - start,
  })

  if (error) return NextResponse.json({ error: error.message }, { status: 500 })
  return NextResponse.json({ data, meta: { page, limit, total: count } })
}

export async function POST(req: NextRequest) {
  const start = Date.now()
  const auth = await validateApiKey(req)
  if ('error' in auth) {
    return NextResponse.json({ error: auth.error }, { status: auth.status })
  }

  const body = await req.json()
  const { data, error } = await supabase.from('items').insert(body).select().single()

  await supabase.from('request_logs').insert({
    api_key_id: auth.apiKey.id,
    method: 'POST',
    path: '/api/v1/items',
    status_code: error ? 500 : 201,
    response_time_ms: Date.now() - start,
  })

  if (error) return NextResponse.json({ error: error.message }, { status: 500 })
  return NextResponse.json({ data }, { status: 201 })
}
```

> Pro tip: Configure CORS headers in your route handlers by adding Access-Control-Allow-Origin and Access-Control-Allow-Headers to every response for cross-origin API consumers.

**Expected result:** GET /api/v1/items returns paginated JSON. POST /api/v1/items creates a new item. Both validate the API key and log the request.

### 4. Build the API key management dashboard

Prompt V0 to create a dashboard page where users can create new API keys, view existing keys, and see usage statistics. The dashboard uses Server Components for data fetching and a client component for interactive key creation.

```
// Paste this prompt into V0's AI chat:
// Build an API key management dashboard at app/dashboard/page.tsx.
// Requirements:
// - Fetch all api_keys for the current user and display in a shadcn/ui Table
// - Each row shows: key name, key (masked with first 8 chars visible), created_at, last_used_at, status Badge (active/inactive)
// - Add a "Create New Key" Button that opens a Dialog with a form (key name input)
// - On submit, insert into api_keys with a generated UUID key, show the full key once in an Alert
// - Add a "Revoke" Button on each row with an AlertDialog confirmation
// - Show request count per key from request_logs as a stat in each row
// - Use Card components for summary stats at the top: total keys, total requests today, average response time
// - Use Tabs to switch between Keys and Request Logs views
// - Request Logs tab shows a Table of recent logs with method Badge, path, status_code, response_time_ms, and timestamp
```

**Expected result:** The dashboard shows API keys in a Table with status Badges. Creating a key opens a Dialog and displays the key once. The Logs tab shows recent API requests.

## Complete code example

File: `app/api/v1/items/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'Authorization, Content-Type',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
}

export async function OPTIONS() {
  return NextResponse.json({}, { headers: corsHeaders })
}

export async function GET(req: NextRequest) {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Missing API key' },
      { status: 401, headers: corsHeaders }
    )
  }

  const key = authHeader.slice(7)
  const { data: apiKey } = await supabase
    .from('api_keys')
    .select('id')
    .eq('key', key)
    .eq('is_active', true)
    .single()

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Invalid API key' },
      { status: 401, headers: corsHeaders }
    )
  }

  const { searchParams } = new URL(req.url)
  const page = parseInt(searchParams.get('page') ?? '1')
  const limit = Math.min(parseInt(searchParams.get('limit') ?? '20'), 100)

  const { data, count, error } = await supabase
    .from('items')
    .select('*', { count: 'exact' })
    .range((page - 1) * limit, page * limit - 1)

  if (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 500, headers: corsHeaders }
    )
  }

  return NextResponse.json(
    { data, meta: { page, limit, total: count } },
    { headers: corsHeaders }
  )
}
```

## Common mistakes

- **Exposing the SUPABASE_SERVICE_ROLE_KEY with a NEXT_PUBLIC_ prefix** — Beginners add NEXT_PUBLIC_ to all env vars, but this prefix makes the key visible in the browser bundle, bypassing all Row Level Security. Fix: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without any prefix. Only SUPABASE_URL and SUPABASE_ANON_KEY should use NEXT_PUBLIC_ if needed on the client.
- **Not handling CORS in API route handlers** — Browsers block cross-origin requests by default. Without CORS headers, external frontends and mobile apps cannot call your API. Fix: Add an OPTIONS handler that returns Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods. Include these headers in all response objects.
- **Using request.json() for all request parsing** — GET requests do not have a body, so calling req.json() throws an error. Also, some webhook payloads need the raw body for signature verification. Fix: Use URL searchParams for GET query parameters. Only call req.json() for POST/PUT/PATCH methods after checking the request method.

## Best practices

- Store all secret keys (SUPABASE_SERVICE_ROLE_KEY, API_SECRET_KEY) in V0's Vars tab without the NEXT_PUBLIC_ prefix
- Use V0's prompt queuing to generate multiple route handlers sequentially — queue prompts for each resource (users, products, orders) while the first generates
- Always return consistent JSON error responses with an error field and appropriate HTTP status codes (401, 403, 429, 500)
- Add request logging to every route handler to enable usage analytics and debugging in your developer dashboard
- Set up CORS headers in a shared utility to avoid repeating the same headers in every route handler
- Use Supabase RLS policies so API keys can only be managed by their owner, even if someone gains direct database access
- Use Design Mode (Option+D) to visually adjust the dashboard layout and Table styling without spending V0 credits

## Frequently asked questions

### Can I use V0's free tier to build this API backend?

Yes. V0's free tier gives you $5 in monthly credits, which is enough to scaffold the API routes and dashboard. Supabase's free tier handles the database, and Vercel's free tier hosts the API with generous serverless function limits.

### How do I deploy the API to production?

Click Share in V0, then go to the Publish tab and click Publish to Production. Your API routes become live Vercel serverless functions in 30-60 seconds. Alternatively, connect to GitHub via the Git panel and merge via pull request.

### How do I add environment variables for Supabase?

Open the Vars tab in V0's editor and add SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (without NEXT_PUBLIC_ prefix since these are server-side only). If you connected Supabase via the Connect panel, the URL and anon key are auto-provisioned.

### Can I add more API endpoints later?

Yes. Each endpoint is an independent route handler file. Just prompt V0 to create a new file at app/api/v1/your-resource/route.ts and it will scaffold the full CRUD handlers with the same middleware pattern.

### How does rate limiting work in a serverless environment?

Each API request checks the Supabase request_logs table for the number of requests from that API key in the last 60 seconds. Supabase handles concurrent writes safely, so even under high load, rate counts stay accurate across serverless function instances.

### Can external mobile apps call this API?

Yes. The API routes include CORS headers (Access-Control-Allow-Origin) so any frontend or mobile app can make authenticated requests. Just include the API key in the Authorization: Bearer header.

### Can RapidDev help build a custom API backend?

Yes. RapidDev has built 600+ applications including complex API backends with authentication, rate limiting, and usage billing. Book a free consultation at rapiddev.com to scope your project.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/api-backend
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/api-backend
