Skip to main content

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

  1. Use TypeScript - Define request/response types
  2. Validate input - Never trust client data
  3. Handle errors - Return consistent error format
  4. Check authentication - Protect sensitive routes
  5. Log appropriately - For debugging and monitoring
  6. Return proper status codes - 200, 201, 400, 401, 404, 500