AgentSkillsCN

Route Handlers

路由处理器

SKILL.md

Next.js Route Handlers & Server Actions


name: route-handlers description: "Next.js Route Handlers, Server Actions, and Middleware patterns. Covers REST API design with Route Handlers, form handling with Server Actions, middleware for auth/redirects/headers, request/response patterns, and streaming responses. Use this skill when building API endpoints or server-side logic in Next.js." license: MIT metadata: author: Balazs Barta version: "0.1.0"

Overview

This skill covers three core server-side patterns in Next.js:

  • Route Handlers: Build REST APIs and handle HTTP requests
  • Server Actions: Handle forms and mutations on the server
  • Middleware: Intercept and modify requests/responses globally

Quick Lookup: Next.js Documentation

Always check the Next.js llms.txt first for authoritative information on:

  • Route Handlers API reference
  • Server Actions details
  • Middleware configuration and patterns

Route Handlers Basics

Route Handlers are created in app/api/ directory with route.ts or route.js files:

code
app/
  api/
    users/
      route.ts          // /api/users endpoint
    users/
      [id]/
        route.ts        // /api/users/[id] dynamic endpoint

Supported HTTP Methods

Route handlers accept these named exports:

typescript
export async function GET(request: NextRequest) {}
export async function POST(request: NextRequest) {}
export async function PUT(request: NextRequest) {}
export async function PATCH(request: NextRequest) {}
export async function DELETE(request: NextRequest) {}
export async function HEAD(request: NextRequest) {}
export async function OPTIONS(request: NextRequest) {}

Request & Response Patterns

NextRequest and NextResponse

Route handlers receive NextRequest objects and return NextResponse:

typescript
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  // Access request properties
  const url = request.nextUrl;
  const searchParams = url.searchParams;
  const pathname = url.pathname;

  // Return response
  return NextResponse.json({ message: 'Hello' });
}

Accessing Headers and Cookies

typescript
import { headers, cookies } from 'next/headers';

export async function GET(request: NextRequest) {
  // Get headers
  const headersList = await headers();
  const authorization = headersList.get('authorization');

  // Get/set cookies
  const cookieStore = await cookies();
  const token = cookieStore.get('sessionToken')?.value;

  cookieStore.set('newCookie', 'value', {
    httpOnly: true,
    secure: true
  });

  return NextResponse.json({ token });
}

URL Parameters and Query Strings

typescript
// Dynamic routes: /api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const userId = params.id;
  return NextResponse.json({ userId });
}

// Query strings: /api/users?page=1&limit=10
export async function GET(request: NextRequest) {
  const page = request.nextUrl.searchParams.get('page');
  const limit = request.nextUrl.searchParams.get('limit');
  return NextResponse.json({ page, limit });
}

Parsing Request Bodies

typescript
// Parse JSON
export async function POST(request: NextRequest) {
  const body = await request.json();
  return NextResponse.json({ received: body });
}

// Parse FormData
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const name = formData.get('name');
  const file = formData.get('file'); // File object
  return NextResponse.json({ name });
}

// Parse text/plain
export async function POST(request: NextRequest) {
  const text = await request.text();
  return NextResponse.json({ text });
}

Server Actions

Server Actions are asynchronous functions that run on the server. Mark them with the "use server" directive.

Basic Server Action (Inline)

typescript
'use client';

import { useState } from 'react';

export default function FormComponent() {
  async function handleSubmit(formData: FormData) {
    'use server';
    const name = formData.get('name');
    // Database mutation here
  }

  return (
    <form action={handleSubmit}>
      <input name="name" required />
      <button type="submit">Submit</button>
    </form>
  );
}

Dedicated Server Action File

typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;

  // Database operation
  const post = await db.posts.create({ title });

  // Revalidate cache
  revalidatePath('/blog');

  return post;
}

useActionState Hook

typescript
'use client';

import { useActionState } from 'react';
import { createPost } from '@/app/actions';

export default function PostForm() {
  const [state, formAction, isPending] = useActionState(
    createPost,
    { error: null }
  );

  return (
    <form action={formAction}>
      <input name="title" required />
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create'}
      </button>
      {state.error && <p>{state.error}</p>}
    </form>
  );
}

useFormStatus Hook

typescript
'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

Route Handlers vs Server Actions

AspectRoute HandlersServer Actions
Use caseREST APIs, webhooks, complex logicForms, simple mutations
TriggerHTTP request (fetch, browser nav)Form submission, client function call
Response typeHTTP response (JSON, streams)Return values, serialized data
Best forExternal API consumers, webhooksInternal app forms, mutations
Error handlingHTTP status codesExceptions, return error objects

Middleware

Middleware runs before requests reach Route Handlers or pages. Create middleware.ts at your project root:

typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  // Check authentication
  const token = request.cookies.get('token');

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

// Configure which routes use middleware
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/admin/:path*',
    '/api/:path*',
  ],
};

Common Middleware Patterns

Authentication:

typescript
const token = request.cookies.get('authToken');
if (!token) {
  return NextResponse.redirect(new URL('/login', request.url));
}

Add Headers:

typescript
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'value');
return response;

Internationalization:

typescript
const locale = request.headers.get('accept-language')?.split(',')[0] || 'en';
request.nextUrl.pathname = `/${locale}${request.nextUrl.pathname}`;
return NextResponse.rewrite(request.nextUrl);

CORS Handling

Set CORS headers in Route Handlers:

typescript
export async function GET(request: NextRequest) {
  return NextResponse.json(
    { data: 'Hello' },
    {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST',
        'Access-Control-Allow-Headers': 'Content-Type',
      },
    }
  );
}

export async function OPTIONS(request: NextRequest) {
  return NextResponse.json(null, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}

Rate Limiting Patterns

Simple in-memory rate limiting:

typescript
const requestCounts = new Map<string, { count: number; resetTime: number }>();

function isRateLimited(ip: string, maxRequests: number = 10, windowMs: number = 60000): boolean {
  const now = Date.now();
  const record = requestCounts.get(ip);

  if (!record || now > record.resetTime) {
    requestCounts.set(ip, { count: 1, resetTime: now + windowMs });
    return false;
  }

  record.count++;
  return record.count > maxRequests;
}

export async function GET(request: NextRequest) {
  const ip = request.ip || 'anonymous';

  if (isRateLimited(ip)) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  return NextResponse.json({ data: 'Hello' });
}

Input Validation with Zod

Use Zod for schema validation:

typescript
import { z } from 'zod';

const createUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validatedData = createUserSchema.parse(body);

    // Safe to use validatedData
    const user = await db.users.create(validatedData);
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Invalid input', details: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}

Error Handling Patterns

Standardized error responses:

typescript
interface ErrorResponse {
  error: string;
  code: string;
  message?: string;
}

export async function GET(request: NextRequest) {
  try {
    const data = await fetchData();
    return NextResponse.json(data);
  } catch (error) {
    if (error instanceof NotFoundError) {
      return NextResponse.json(
        { error: 'Not found', code: 'NOT_FOUND' } as ErrorResponse,
        { status: 404 }
      );
    }

    if (error instanceof ValidationError) {
      return NextResponse.json(
        { error: 'Invalid input', code: 'VALIDATION_ERROR', message: error.message } as ErrorResponse,
        { status: 400 }
      );
    }

    return NextResponse.json(
      { error: 'Internal server error', code: 'INTERNAL_ERROR' } as ErrorResponse,
      { status: 500 }
    );
  }
}

Streaming Responses

Stream data for long-running operations:

typescript
export async function GET(request: NextRequest) {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`);
      }
      controller.close();
    },
  });

  return new NextResponse(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    },
  });
}

Resources

For detailed patterns and examples, see:

  • /skills/route-handlers/references/route-handler-patterns.md - Comprehensive Route Handler patterns
  • /skills/route-handlers/references/server-action-patterns.md - Server Action examples and best practices