kb.erickguedes.com
Next.js: React Full-Stack

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. Use revalidatePath e revalidateTag para atualizar cache após mutations. useActionState gerencia estado do formulário e loading.