Arquitetando um CRUD Escalável no Next.js

Arquitetura Moderna para CRUDs no Next.js
- 👉 Parte 1: Arquitetando um CRUD Escalável (Você está aqui)
- Parte 2: Axios, Services, Server Components e Server Actions (Em breve)
- Parte 3: TanStack Query (Queries, Cache e Mutations) (Em breve)
- Parte 4: React Hook Form + Zod + Server Actions (Em breve)
- Parte 5: Search Params, Filtros e Paginação (Em breve)
- Parte 6: Optimistic Updates e UX (Em breve)
O Desafio do Mundo Real
Depois de alguns anos trabalhando com desenvolvimento de software, você percebe que criar um CRUD (Create, Read, Update, Delete) não é a parte difícil.
O verdadeiro desafio é manter esse CRUD saudável depois de meses ou anos de evolução.
No começo, tudo parece simples:
- uma página;
- uma tabela;
- alguns formulários;
- algumas chamadas HTTP.
Mas aplicações reais crescem.
Novos filtros aparecem, regras de negócio aumentam, diferentes telas começam a reutilizar os mesmos dados e novos desenvolvedores entram no projeto.
É nesse momento que uma arquitetura mal definida começa a cobrar seu preço.
Os problemas aparecem rapidamente:
- componentes fazendo chamadas HTTP diretamente;
- regras de negócio espalhadas pela interface;
- validações duplicadas;
- estados de loading inconsistentes;
- dificuldade para alterar uma funcionalidade sem quebrar outra.
Criar um CRUD é fácil.
Criar um CRUD que continue simples depois de milhares de linhas de código é o verdadeiro desafio.
O Que NÃO Veremos Nesta Série
Existem várias formas modernas de construir aplicações utilizando Next.js.
Algumas abordagens populares:
- tRPC;
- GraphQL;
- Server Actions diretamente conectadas ao banco;
- ORMs full-stack;
- arquiteturas orientadas a domínio.
Todas possuem excelentes aplicações.
Porém, nesta série vamos trabalhar com uma arquitetura baseada em:
- Next.js App Router;
- APIs REST;
- Axios;
- Services;
- Server Components;
- Server Actions;
- TanStack Query;
- React Hook Form;
- Zod.
O objetivo não é ensinar uma biblioteca específica.
O objetivo é entender como organizar responsabilidades.
O Problema dos CRUDs Tradicionais
Em aplicações menores, é comum encontrar algo parecido:
"use client";
import axios from "axios";
import { useEffect, useState } from "react";
export default function UsersPage() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios
.get("https://api.exemplo.com/users")
.then((response) => setUsers(response.data))
.catch(console.error)
.finally(() => setLoading(false));
}, []);
if (loading) {
return <p>Carregando...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
Funciona?
Sim.
Mas esse componente acumulou responsabilidades demais.
Ele sabe:
- qual cliente HTTP está sendo usado;
- qual endpoint chamar;
- como tratar erros;
- como controlar loading;
- como armazenar estado;
- como renderizar a interface.
O problema não é esse código existir.
O problema é quando esse padrão se espalha por 50 telas diferentes.
Imagine trocar:
- Axios por Fetch;
- REST por GraphQL;
- autenticação;
- tratamento de erros.
A alteração deixa de ser localizada e começa a virar uma operação de risco.
Isso é alto acoplamento.
Pensando em Camadas
Para resolver esse problema, precisamos separar responsabilidades.
Uma aplicação moderna em Next.js pode ser organizada assim:
┌─────────────────────────┐
│ Components │
│ Interface e interação │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Custom Hooks │
│ Lógica reutilizável │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ TanStack Query │
│ Cache e estado remoto │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Server Actions │
│ Mutações no servidor │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Services │
│ Acesso aos dados │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Axios │
│ Comunicação HTTP │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ API │
└─────────────────────────┘
Cada camada possui uma responsabilidade.
Um componente React não precisa saber como uma requisição HTTP funciona.
Um Service não precisa saber quem está renderizando os dados.
Uma Server Action não deve conter toda regra de negócio.
A Regra Mais Importante
Durante toda esta série vamos seguir uma regra simples:
Cada camada conhece apenas as responsabilidades necessárias da próxima camada.
Na prática:
✅ Componentes não conhecem Axios.
✅ Axios não conhece React.
✅ Services não conhecem componentes.
✅ Server Actions orquestram operações executadas no servidor.
✅ TanStack Query controla cache e sincronização no cliente.
Essa separação facilita:
- manutenção;
- testes;
- evolução;
- troca de tecnologias.
Estrutura de Pastas
Uma organização possível usando Next.js App Router:
src/
├── app/
│ └── Rotas, layouts e Server Components
├── actions/
│ └── Server Actions para mutações
├── components/
│ └── Componentes visuais
├── hooks/
│ └── Hooks personalizados
├── services/
│ └── Comunicação com APIs
├── schemas/
│ └── Validações utilizando Zod
├── types/
│ └── Tipagens compartilhadas
└── lib/
└── Axios e utilitários
Entendendo Cada Camada
app/
Responsável pela estrutura de rotas do Next.js.
Aqui normalmente ficam:
- páginas;
- layouts;
- Server Components;
- carregamento inicial de dados.
components/
Componentes visuais.
Eles devem se preocupar principalmente com:
- apresentação;
- interação do usuário;
- composição.
Quanto menos regra de negócio existir aqui, melhor.
services/
A camada de acesso aos dados.
Exemplo:
UsersService.findMany()
O componente não precisa saber:
- qual URL chamar;
- quais headers enviar;
- como tratar autenticação.
Ele apenas solicita uma operação.
actions/
As Server Actions são funções executadas no servidor.
Elas são especialmente úteis para:
- criar registros;
- editar dados;
- excluir informações;
- executar operações sensíveis.
Exemplo:
"use server";
import { usersService } from "@/services/users.service";
export async function createUserAction(data: FormData) {
const name = data.get("name");
return usersService.create({
name: String(name),
});
}
A Action funciona como uma ponte segura entre o cliente e o servidor.
Ela pode:
- validar permissões;
- validar dados;
- chamar Services;
- atualizar cache com
revalidatePath.
schemas/
Centraliza validações.
Exemplo:
const userSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
});
Assim o mesmo contrato pode ser utilizado em:
- formulários;
- actions;
- APIs.
lib/
Configurações compartilhadas.
Exemplos:
- Axios;
- clientes externos;
- helpers;
- configurações globais.
O Fluxo de Dados na Prática
No Next.js moderno temos diferentes fluxos dependendo da necessidade.
Leitura de Dados no Servidor
Quando uma página precisa apenas carregar dados:
Server Component
↓
UsersService.findMany()
↓
Axios
↓
API
O carregamento acontece no servidor, evitando enviar lógica desnecessária para o navegador.
Leitura Interativa no Cliente
Quando temos:
- filtros;
- tabelas dinâmicas;
- paginação;
- busca;
podemos utilizar TanStack Query:
Client Component
↓
useUsers()
↓
TanStack Query
↓
UsersService.findMany()
Aqui ganhamos:
- cache;
- refetch automático;
- controle de loading;
- sincronização.
Escrita de Dados
Para criar, editar ou excluir:
Formulário
↓
Server Action
↓
UsersService.create()
↓
Axios
↓
API
A Action mantém a operação sensível no servidor.
O Que Ganharemos Com Essa Arquitetura?
Ao final desta série teremos uma base onde:
✅ Componentes ficam focados em interface.
✅ APIs ficam isoladas.
✅ Regras de acesso aos dados ficam centralizadas.
✅ Cache não polui componentes.
✅ Formulários compartilham validações.
✅ Filtros e paginação podem ser tratados de forma previsível.
✅ Mudanças futuras terão menor impacto.
Conclusão
Um CRUD simples pode ser criado em poucas horas.
O desafio real é criar uma estrutura que continue funcionando quando o projeto crescer.
A arquitetura moderna do Next.js nos entrega ferramentas poderosas:
- Server Components;
- Server Actions;
- TanStack Query;
- validação com Zod;
- separação por Services.
Mas ferramentas sozinhas não resolvem problemas.
O que realmente escala é ter clareza sobre responsabilidades.
No próximo artigo vamos criar uma camada profissional de comunicação utilizando Axios, interceptors, autenticação e Services reutilizáveis.
Essa será a base para todas as consultas e mutações que construiremos durante a série.