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.
What you're building
Every SaaS product and mobile app eventually needs a backend API. Whether you are building a data service for a mobile app or exposing business logic for partners, a clean REST API with authentication and rate limiting is the foundation.
V0 makes this dramatically faster by generating Next.js API route handlers from natural language prompts. You describe the endpoints you need, and V0 scaffolds the route files, types, and validation logic. Pair that with Supabase via the Connect panel for instant database provisioning, and you have a full backend running on Vercel in minutes.
The architecture uses Next.js App Router route handlers (app/api/v1/*/route.ts) for each resource endpoint, a shared middleware module for API key validation and rate limiting, Server Components for the developer dashboard, and Supabase for storing API keys, request logs, and your application data.
Final result
A fully functional REST API with key-based auth, per-key rate limiting, request logging, and a developer dashboard for managing keys and monitoring usage.
Tech stack
Prerequisites
- 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)
Build steps
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.
1// Paste this prompt into V0's AI chat:2// Build a REST API management system. Create a Supabase schema with these tables:3// 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)4// 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)5// 3. rate_limits: id (uuid PK), api_key_id (uuid FK), window_start (timestamptz), request_count (int default 1)6// Add RLS policies so users can only see their own API keys.7// 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.
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.
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function validateApiKey(req: NextRequest) {10 const authHeader = req.headers.get('Authorization')11 if (!authHeader?.startsWith('Bearer ')) {12 return { error: 'Missing API key', status: 401 }13 }1415 const key = authHeader.slice(7)16 const { data: apiKey } = await supabase17 .from('api_keys')18 .select('id, user_id, is_active')19 .eq('key', key)20 .eq('is_active', true)21 .single()2223 if (!apiKey) {24 return { error: 'Invalid API key', status: 401 }25 }2627 const windowStart = new Date(Date.now() - 60000).toISOString()28 const { count } = await supabase29 .from('request_logs')30 .select('*', { count: 'exact', head: true })31 .eq('api_key_id', apiKey.id)32 .gte('created_at', windowStart)3334 if ((count ?? 0) >= 60) {35 return { error: 'Rate limit exceeded', status: 429 }36 }3738 return { apiKey }39}Expected result: The middleware module is created. It exports a validateApiKey function that returns either the API key object or an error response.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { validateApiKey } from '@/lib/api-middleware'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910export async function GET(req: NextRequest) {11 const start = Date.now()12 const auth = await validateApiKey(req)13 if ('error' in auth) {14 return NextResponse.json({ error: auth.error }, { status: auth.status })15 }1617 const { searchParams } = new URL(req.url)18 const page = parseInt(searchParams.get('page') ?? '1')19 const limit = Math.min(parseInt(searchParams.get('limit') ?? '20'), 100)2021 const { data, count, error } = await supabase22 .from('items')23 .select('*', { count: 'exact' })24 .range((page - 1) * limit, page * limit - 1)2526 await supabase.from('request_logs').insert({27 api_key_id: auth.apiKey.id,28 method: 'GET',29 path: '/api/v1/items',30 status_code: error ? 500 : 200,31 response_time_ms: Date.now() - start,32 })3334 if (error) return NextResponse.json({ error: error.message }, { status: 500 })35 return NextResponse.json({ data, meta: { page, limit, total: count } })36}3738export async function POST(req: NextRequest) {39 const start = Date.now()40 const auth = await validateApiKey(req)41 if ('error' in auth) {42 return NextResponse.json({ error: auth.error }, { status: auth.status })43 }4445 const body = await req.json()46 const { data, error } = await supabase.from('items').insert(body).select().single()4748 await supabase.from('request_logs').insert({49 api_key_id: auth.apiKey.id,50 method: 'POST',51 path: '/api/v1/items',52 status_code: error ? 500 : 201,53 response_time_ms: Date.now() - start,54 })5556 if (error) return NextResponse.json({ error: error.message }, { status: 500 })57 return NextResponse.json({ data }, { status: 201 })58}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.
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.
1// Paste this prompt into V0's AI chat:2// Build an API key management dashboard at app/dashboard/page.tsx.3// Requirements:4// - Fetch all api_keys for the current user and display in a shadcn/ui Table5// - Each row shows: key name, key (masked with first 8 chars visible), created_at, last_used_at, status Badge (active/inactive)6// - Add a "Create New Key" Button that opens a Dialog with a form (key name input)7// - On submit, insert into api_keys with a generated UUID key, show the full key once in an Alert8// - Add a "Revoke" Button on each row with an AlertDialog confirmation9// - Show request count per key from request_logs as a stat in each row10// - Use Card components for summary stats at the top: total keys, total requests today, average response time11// - Use Tabs to switch between Keys and Request Logs views12// - Request Logs tab shows a Table of recent logs with method Badge, path, status_code, response_time_ms, and timestampExpected 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
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89const corsHeaders = {10 'Access-Control-Allow-Origin': '*',11 'Access-Control-Allow-Headers': 'Authorization, Content-Type',12 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',13}1415export async function OPTIONS() {16 return NextResponse.json({}, { headers: corsHeaders })17}1819export async function GET(req: NextRequest) {20 const authHeader = req.headers.get('Authorization')21 if (!authHeader?.startsWith('Bearer ')) {22 return NextResponse.json(23 { error: 'Missing API key' },24 { status: 401, headers: corsHeaders }25 )26 }2728 const key = authHeader.slice(7)29 const { data: apiKey } = await supabase30 .from('api_keys')31 .select('id')32 .eq('key', key)33 .eq('is_active', true)34 .single()3536 if (!apiKey) {37 return NextResponse.json(38 { error: 'Invalid API key' },39 { status: 401, headers: corsHeaders }40 )41 }4243 const { searchParams } = new URL(req.url)44 const page = parseInt(searchParams.get('page') ?? '1')45 const limit = Math.min(parseInt(searchParams.get('limit') ?? '20'), 100)4647 const { data, count, error } = await supabase48 .from('items')49 .select('*', { count: 'exact' })50 .range((page - 1) * limit, page * limit - 1)5152 if (error) {53 return NextResponse.json(54 { error: error.message },55 { status: 500, headers: corsHeaders }56 )57 }5859 return NextResponse.json(60 { data, meta: { page, limit, total: count } },61 { headers: corsHeaders }62 )63}Customization ideas
Add webhook notifications
Send a webhook to a user-configured URL whenever specific API events occur, like a new resource being created or rate limits being hit.
Add API versioning
Create a v2 folder alongside v1 to support multiple API versions simultaneously, with a middleware that routes based on the URL prefix.
Add usage-based billing
Track API request counts per billing period and integrate with Stripe Metered Billing to charge customers based on their actual usage.
Common pitfalls
Pitfall: Exposing the SUPABASE_SERVICE_ROLE_KEY with a NEXT_PUBLIC_ prefix
How to avoid: 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.
Pitfall: Not handling CORS in API route handlers
How to avoid: 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.
Pitfall: Using request.json() for all request parsing
How to avoid: 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
AI prompts to try
Copy these prompts to build this project faster.
I'm building a REST API backend with Next.js App Router and Supabase. I need API route handlers at app/api/v1/items/route.ts with GET (paginated list) and POST (create) endpoints. Each route should validate an API key from the Authorization header against a Supabase api_keys table, check rate limits (60 requests per minute per key), and log the request. Give me the complete TypeScript code using @supabase/supabase-js.
Create a rate limiting middleware for my Next.js API routes. The middleware should: read the API key from the Authorization Bearer header, look it up in a Supabase api_keys table, count requests in the last 60 seconds from a request_logs table, return 429 if over 60 requests, and log every request with method, path, status code, and response time in milliseconds. Export it as a reusable function from lib/api-middleware.ts.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation