Route Handlers e Server Actions
Aula 3 de 6
Route Handlers
Route handlers permitem criar APIs REST dentro do App Router usando o arquivo route.ts.
app/
└── api/
├── users/
│ ├── route.ts # GET /api/users, POST /api/users
│ └── [id]/
│ └── route.ts # GET /api/users/123
└── posts/
└── route.ts
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get('page')) || 1;
const limit = Number(searchParams.get('limit')) || 10;
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
});
const total = await db.user.count();
return NextResponse.json({
data: users,
meta: { page, limit, total, totalPages: Math.ceil(total / limit) }
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
const { name, email } = body;
if (!name || !email) {
return NextResponse.json(
{ error: 'Nome e email são obrigatórios' },
{ status: 400 }
);
}
const user = await db.user.create({ data: { name, email } });
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
type Params = { params: { id: string } };
export async function GET(request: NextRequest, { params }: Params) {
const id = Number(params.id);
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json(
{ error: 'Usuário não encontrado' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(request: NextRequest, { params }: Params) {
const id = Number(params.id);
const body = await request.json();
const user = await db.user.update({
where: { id },
data: body
});
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, { params }: Params) {
const id = Number(params.id);
await db.user.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
Headers, Cookies e Redirect
// app/api/auth/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(request: NextRequest) {
// Headers da requisição
const headersList = await headers();
const userAgent = headersList.get('user-agent');
// Ler cookies
const cookieStore = await cookies();
const token = cookieStore.get('session-token');
if (!token) {
// Redirect via NextResponse
return NextResponse.redirect(new URL('/login', request.url));
}
// Definir cookie
const response = NextResponse.json({ authenticated: true });
response.cookies.set('session-token', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 7 dias
});
return response;
}
Server Actions
Server Actions permitem executar código no servidor diretamente de componentes.
// app/actions/users.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email()
});
export async function createUser(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email')
};
const parsed = userSchema.safeParse(rawData);
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
const user = await db.user.create({ data: parsed.data });
revalidatePath('/users');
return { success: true, user };
}
export async function deleteUser(id: number) {
await db.user.delete({ where: { id } });
revalidatePath('/users');
}
export async function updatePost(id: number, data: { title?: string; content?: string }) {
await db.post.update({ where: { id }, data });
revalidatePath('/posts');
}
Uso com useActionState
// app/users/new/page.tsx
'use client';
import { useActionState } from 'react';
import { createUser } from '@/app/actions/users';
const initialState = { error: null, success: false };
export default function NewUserPage() {
const [state, formAction, isPending] = useActionState(createUser, initialState);
if (state.success) {
return <div>Usuário criado com sucesso!</div>;
}
return (
<form action={formAction}>
<div>
<label>Nome</label>
<input
type="text"
name="name"
required
className="border p-2 rounded"
/>
{state.error?.name && (
<span className="text-red-500">{state.error.name}</span>
)}
</div>
<div>
<label>Email</label>
<input
type="email"
name="email"
required
className="border p-2 rounded"
/>
{state.error?.email && (
<span className="text-red-500">{state.error.email}</span>
)}
</div>
<button
type="submit"
disabled={isPending}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
{isPending ? 'Salvando...' : 'Criar Usuário'}
</button>
</form>
);
}
Mutations com Server Actions
// app/todos/todo-list.tsx
'use client';
import { useActionState } from 'react';
import { addTodo, toggleTodo, deleteTodo } from '@/app/actions/todos';
type Todo = { id: number; title: string; completed: boolean };
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [state, formAction] = useActionState(addTodo, { error: null });
return (
<div>
<form action={formAction}>
<input type="text" name="title" required placeholder="Nova tarefa..." />
<button type="submit">Adicionar</button>
</form>
<ul>
{initialTodos.map(todo => (
<li key={todo.id}>
<form action={toggleTodo}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">
{todo.completed ? '✓' : '○'}
</button>
</form>
<span className={todo.completed ? 'line-through' : ''}>
{todo.title}
</span>
<form action={deleteTodo}>
<input type="hidden" name="id" value={todo.id} />
<button type="submit">🗑</button>
</form>
</li>
))}
</ul>
</div>
);
}
// app/actions/todos.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function addTodo(formData: FormData) {
const title = formData.get('title')?.toString();
if (!title?.trim()) return { error: 'Título é obrigatório' };
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
export async function toggleTodo(formData: FormData) {
const id = Number(formData.get('id'));
const todo = await db.todo.findUnique({ where: { id } });
if (!todo) return;
await db.todo.update({
where: { id },
data: { completed: !todo.completed }
});
revalidatePath('/todos');
}
export async function deleteTodo(formData: FormData) {
const id = Number(formData.get('id'));
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}
Lab: Exercício - API + Server Actions
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
const posts = [
{ id: 1, title: 'Post 1', content: 'Conteúdo do post 1' },
{ id: 2, title: 'Post 2', content: 'Conteúdo do post 2' }
];
export async function GET() {
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = { id: posts.length + 1, ...body };
posts.push(post);
return NextResponse.json(post, { status: 201 });
}
# Testar route handlers
curl http://localhost:3000/api/posts
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Post 3","content":"Conteúdo do post 3"}'
Route handlers (route.ts) substituem API routes do Pages Router. Server Actions (
'use server') permitem mutations sem criar endpoints. UserevalidatePatherevalidateTagpara atualizar cache após mutations.useActionStategerencia estado do formulário e loading.