Adding API Routes
Learn how to create backend API endpoints in the Vertical Template.
Basic API Route
Create an API route by adding a route.ts file:
// src/app/api/hello/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello, World!' });
}
Access at: http://localhost:3000/api/hello
HTTP Methods
Handle different HTTP methods:
// src/app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/projects
export async function GET() {
const projects = await getProjects();
return NextResponse.json(projects);
}
// POST /api/projects
export async function POST(request: NextRequest) {
const body = await request.json();
const project = await createProject(body);
return NextResponse.json(project, { status: 201 });
}
Protected Routes
Require user authentication:
// src/app/api/protected/route.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';
export async function GET() {
const session = await auth();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
return NextResponse.json({
message: `Hello, ${session.user?.name}`,
userId: session.user?.id,
});
}
Auth Helper Function
Create a reusable auth wrapper:
// src/lib/api-auth.ts
import { auth } from '@/auth';
import { NextRequest, NextResponse } from 'next/server';
type AuthenticatedHandler = (
request: NextRequest,
session: Session
) => Promise<NextResponse>;
export function withAuth(handler: AuthenticatedHandler) {
return async (request: NextRequest) => {
const session = await auth();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
return handler(request, session);
};
}
Usage:
// src/app/api/protected/route.ts
import { withAuth } from '@/lib/api-auth';
export const GET = withAuth(async (request, session) => {
return NextResponse.json({
message: `Hello, ${session.user?.name}`,
});
});
Dynamic Routes
Create routes with URL parameters:
// src/app/api/projects/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface Props {
params: { id: string };
}
// GET /api/projects/:id
export async function GET(request: NextRequest, { params }: Props) {
const project = await getProject(params.id);
if (!project) {
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
return NextResponse.json(project);
}
// PUT /api/projects/:id
export async function PUT(request: NextRequest, { params }: Props) {
const body = await request.json();
const project = await updateProject(params.id, body);
return NextResponse.json(project);
}
// DELETE /api/projects/:id
export async function DELETE(request: NextRequest, { params }: Props) {
await deleteProject(params.id);
return NextResponse.json({ success: true });
}
Query Parameters
Access URL query parameters:
// src/app/api/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/search?q=keyword&page=1
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q') || '';
const page = parseInt(searchParams.get('page') || '1');
const results = await search(query, page);
return NextResponse.json({
query,
page,
results,
});
}
Request Body Validation
Validate incoming request data:
// src/app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const ProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
status: z.enum(['draft', 'active', 'completed']),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = ProjectSchema.parse(body);
const project = await createProject(validated);
return NextResponse.json(project, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.errors },
{ status: 400 }
);
}
throw error;
}
}
Proxy to External API
Forward requests to an external service:
// src/app/api/external/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth';
const EXTERNAL_API = process.env.EXTERNAL_API_URL;
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
const session = await auth();
const path = params.path.join('/');
const response = await fetch(`${EXTERNAL_API}/${path}`, {
headers: {
Authorization: `Bearer ${session?.accessToken}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
return NextResponse.json(data, { status: response.status });
}
Error Handling
Consistent error responses:
// src/lib/api-errors.ts
import { NextResponse } from 'next/server';
export class APIError extends Error {
constructor(
message: string,
public status: number = 500,
public code?: string
) {
super(message);
}
}
export function handleAPIError(error: unknown) {
console.error('API Error:', error);
if (error instanceof APIError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.status }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
Usage:
// src/app/api/projects/route.ts
import { APIError, handleAPIError } from '@/lib/api-errors';
export async function GET() {
try {
const projects = await getProjects();
return NextResponse.json(projects);
} catch (error) {
return handleAPIError(error);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
if (!body.name) {
throw new APIError('Name is required', 400, 'MISSING_NAME');
}
const project = await createProject(body);
return NextResponse.json(project, { status: 201 });
} catch (error) {
return handleAPIError(error);
}
}
Streaming Responses
For long-running operations or SSE:
// src/app/api/stream/route.ts
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const message = `data: ${JSON.stringify({ count: i })}\n\n`;
controller.enqueue(encoder.encode(message));
await new Promise(resolve => setTimeout(resolve, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Rate Limiting
Basic rate limiting example:
// src/lib/rate-limit.ts
const rateLimit = new Map<string, { count: number; timestamp: number }>();
export function checkRateLimit(
ip: string,
limit: number = 100,
windowMs: number = 60000
): boolean {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now - record.timestamp > windowMs) {
rateLimit.set(ip, { count: 1, timestamp: now });
return true;
}
if (record.count >= limit) {
return false;
}
record.count++;
return true;
}
Usage:
// src/app/api/limited/route.ts
import { checkRateLimit } from '@/lib/rate-limit';
export async function GET(request: NextRequest) {
const ip = request.ip || 'unknown';
if (!checkRateLimit(ip)) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
return NextResponse.json({ message: 'OK' });
}
API Route Structure
src/app/api/
├── auth/
│ └── [...nextauth]/
│ └── route.ts # Auth.js endpoints
│
├── eai/
│ ├── [[...rest]]/
│ │ └── route.ts # Catch-all proxy
│ ├── config/
│ │ └── route.ts # Runtime config
│ └── public/
│ └── v3/
│ └── [...path]/
│ └── route.ts # Public API proxy
│
├── projects/
│ ├── route.ts # GET, POST /api/projects
│ └── [id]/
│ └── route.ts # GET, PUT, DELETE /api/projects/:id
│
└── webhooks/
└── [provider]/
└── route.ts # Webhook handlers
Best Practices
- Use TypeScript - Define request/response types
- Validate input - Never trust client data
- Handle errors - Return consistent error format
- Check authentication - Protect sensitive routes
- Log appropriately - For debugging and monitoring
- Return proper status codes - 200, 201, 400, 401, 404, 500
Related Documentation
- Adding Pages - Create frontend pages
- Public API Access - Anonymous API access
- Architecture Overview - System design