Pular para o conteúdo
TypeScript

API Route no Next.js com TypeScript 2026

Route Handlers do App Router são poderosos, mas tipar corretamente request, response e params dinâmicos exige atenção. Veja como fazer do jeito certo.

Por que isso é importante

API Route no Next.js com TypeScript 2026. Route Handlers do App Router são poderosos, mas tipar corretamente request, response e params dinâmicos exige atenção. Veja como fazer do jeito certo.

Route Handlers vs API Routes: o que mudou

No Pages Router, APIs ficavam em pages/api/ e usavam NextApiRequest/NextApiResponse. No App Router, as APIs usam Route Handlers em arquivos route.ts e trabalham com NextRequest/NextResponse da Web API. A abordagem é diferente e os tipos também.

Galera, a grande vantagem é que Route Handlers seguem o padrão Web standard. Se você conhece fetch() e Response, já tá em casa. A tipagem com TypeScript deixa tudo mais previsível.

Primeiro Route Handler: GET tipado

Pra criar uma API no App Router, crie um arquivo route.ts dentro de app/api/. Cada função exportada (GET, POST, PUT, DELETE, PATCH) vira um endpoint. O tipo de retorno é Response ou NextResponse.

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface User {
  id: string;
  name: string;
  email: string;
}

// GET /api/users
export async function GET(request: NextRequest) {
  const users: User[] = [
    { id: '1', name: 'João', email: 'joao@email.com' },
    { id: '2', name: 'Maria', email: 'maria@email.com' },
  ];

  return NextResponse.json(users, { status: 200 });
}

NextResponse.json() serializa o objeto e define o Content-Type automaticamente. O TypeScript infere o tipo do JSON a partir do argumento.

POST com body tipado e validação

No App Router, o body vem do request.json(), que retorna Promise. Sem validação, qualquer coisa pode entrar. Combine com Zod pra ter segurança total.

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user'),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

// POST /api/users
export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const result = CreateUserSchema.safeParse(body);

    if (!result.success) {
      return NextResponse.json(
        { errors: result.error.flatten().fieldErrors },
        { status: 400 }
      );
    }

    const user: CreateUserInput = result.data;
    // user tem tipo seguro aqui

    // Salvar no banco...
    return NextResponse.json(
      { id: 'novo-id', ...user },
      { status: 201 }
    );
  } catch (error) {
    return NextResponse.json(
      { error: 'Erro ao processar requisição' },
      { status: 500 }
    );
  }
}

Params dinâmicos tipados

Rotas dinâmicas como app/api/users/[id]/route.ts recebem os params como segundo argumento. No Next.js 15+, params é uma Promise. A tipagem fica assim:

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface RouteParams {
  params: Promise<{ id: string }>;
}

// GET /api/users/:id
export async function GET(
  request: NextRequest,
  { params }: RouteParams
) {
  const { id } = await params;

  // Buscar user pelo id...
  const user = { id, name: 'João', email: 'joao@email.com' };

  if (!user) {
    return NextResponse.json(
      { error: 'Usuário não encontrado' },
      { status: 404 }
    );
  }

  return NextResponse.json(user);
}

// DELETE /api/users/:id
export async function DELETE(
  request: NextRequest,
  { params }: RouteParams
) {
  const { id } = await params;

  // Deletar user...
  return NextResponse.json(
    { message: `Usuário ${id} removido` },
    { status: 200 }
  );
}

Pra rotas com múltiplos params, como app/api/posts/[postId]/comments/[commentId]/route.ts, a interface fica com todos os segmentos.

interface NestedRouteParams {
  params: Promise<{ postId: string; commentId: string }>;
}

export async function GET(
  request: NextRequest,
  { params }: NestedRouteParams
) {
  const { postId, commentId } = await params;
  // Ambos os params tipados como string
}

Query params e headers tipados

Dá pra extrair query params e headers do NextRequest de forma tipada. O searchParams vem do URL object, e headers do request.

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

const QuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
});

export async function GET(request: NextRequest) {
  // Query params
  const { searchParams } = request.nextUrl;
  const query = QuerySchema.parse({
    page: searchParams.get('page'),
    limit: searchParams.get('limit'),
    search: searchParams.get('search'),
  });
  // query.page é number, query.limit é number

  // Headers
  const authHeader = request.headers.get('authorization');
  const contentType = request.headers.get('content-type');

  // Cookies
  const token = request.cookies.get('session-token')?.value;

  return NextResponse.json({
    page: query.page,
    limit: query.limit,
  });
}

Middleware tipado no Next.js

O middleware do Next.js roda antes de cada request. Dá pra usar pra autenticação, redirecionamento e manipulação de headers. A tipagem é direta com NextRequest e NextResponse.

// middleware.ts (raiz do projeto)
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;
  const isApiRoute = request.nextUrl.pathname.startsWith('/api');
  const isPublicRoute = ['/api/auth/login', '/api/auth/register']
    .includes(request.nextUrl.pathname);

  // Rotas públicas passam direto
  if (isPublicRoute) {
    return NextResponse.next();
  }

  // Rotas de API sem token retornam 401
  if (isApiRoute && !token) {
    return NextResponse.json(
      { error: 'Token obrigatório' },
      { status: 401 }
    );
  }

  // Adicionar header custom
  const response = NextResponse.next();
  response.headers.set('x-request-id', crypto.randomUUID());
  return response;
}

export const config = {
  matcher: ['/api/:path*'],
};

Padrão de resposta consistente

Crie um helper de resposta pra manter o padrão em todas as rotas. Isso facilita manutenção e garante que os clients sempre recebam a mesma estrutura.

// lib/api-response.ts
import { NextResponse } from 'next/server';

interface ApiResponse<T> {
  data?: T;
  error?: string;
  errors?: Record<string, string[]>;
}

export function successResponse<T>(data: T, status = 200) {
  return NextResponse.json<ApiResponse<T>>(
    { data },
    { status }
  );
}

export function errorResponse(error: string, status = 400) {
  return NextResponse.json<ApiResponse<never>>(
    { error },
    { status }
  );
}

export function validationError(errors: Record<string, string[]>) {
  return NextResponse.json<ApiResponse<never>>(
    { error: 'Dados inválidos', errors },
    { status: 422 }
  );
}

// Uso no Route Handler
import { successResponse, errorResponse } from '@/lib/api-response';

export async function GET() {
  const users = await getUsers();
  return successResponse(users);
}

export async function POST(request: NextRequest) {
  const result = CreateUserSchema.safeParse(await request.json());
  if (!result.success) {
    return validationError(result.error.flatten().fieldErrors);
  }
  const user = await createUser(result.data);
  return successResponse(user, 201);
}

Erros comuns em Route Handlers

Atenção

Erro 1: Esquecer o await nos params. No Next.js 15+, params é Promise. Sem await, você recebe o objeto Promise em vez dos valores.

Erro 2: Não tratar body inválido no POST. Se o client manda JSON malformado, request.json() lança exceção. Sempre envolva em try/catch.

Erro 3: Usar NextApiRequest/NextApiResponse no App Router. Esses tipos são do Pages Router. No App Router, use NextRequest/NextResponse.

Erro 4: Retornar objeto direto em vez de NextResponse.json(). Route Handlers precisam retornar um Response object. Retornar um objeto puro gera erro.

Passo a passo: API completa no App Router

  1. Crie a pasta app/api/ com subpastas por recurso (users, posts, etc.)
  2. Crie route.ts em cada pasta com funções GET, POST, PUT, DELETE
  3. Defina interfaces de response e use NextResponse.json() tipado
  4. Crie schemas Zod para validar body e query params
  5. Tipe params dinâmicos com Promise<{ id: string }>
  6. Crie helpers de resposta em lib/api-response.ts
  7. Configure middleware.ts para autenticação global

Checklist: Route Handlers tipados

Checklist: API Next.js + TypeScript

  • Route Handlers usando NextRequest e NextResponse
  • Params dinâmicos tipados com Promise (Next.js 15+)
  • Body validado com Zod em rotas POST/PUT/PATCH
  • Query params validados com z.coerce para conversão
  • Helper de resposta padronizado para toda API
  • Middleware tipado para autenticação
  • Tratamento de erros com try/catch em todas as rotas
  • Tipos de request e response documentados

TypeScript Profissional: Projeto Completo

No CrazyStack você constrói um projeto completo com TypeScript

Node.js e React. APIs tipadas no Next.js

validação com Zod e deploy profissional. Acesse:

/comprar?src=blog-como-criar-api-nextjs-typescript
]
}
]
}
]
}