Antes de código, você precisa entender o fluxo completo de uma requisição. Cada camada tem responsabilidades distintas e saber onde cada problema ocorre poupa horas de debug.
O Ciclo Completo de uma Requisição
meusite.com em um IP (ex: 192.168.1.1) consultando servidores DNS hierarquicamente.| Status Code | Significado | Quando ocorre |
|---|---|---|
200 OK | Sucesso | Requisição processada com sucesso |
201 Created | Recurso criado | POST bem-sucedido (criou entidade no banco) |
301/302 | Redirect | URL movida permanente/temporariamente |
400 Bad Request | Erro do cliente | Payload inválido, falta de campo obrigatório |
401 Unauthorized | Não autenticado | Token ausente ou expirado |
403 Forbidden | Sem permissão | Autenticado mas sem acesso ao recurso |
404 Not Found | Não encontrado | Rota ou recurso inexistente |
422 Unprocessable | Validação falhou | Dados bem formados mas semanticamente inválidos |
500 Server Error | Erro interno | Exceção não tratada no servidor |
Configuração Inicial
Comandos Fundamentais
| Comando | O que faz |
|---|---|
git init | Inicia repositório na pasta atual |
git clone <url> | Clona repositório remoto |
git status | Mostra arquivos modificados/staged |
git add . | Adiciona todos os arquivos ao staging |
git commit -m "msg" | Cria snapshot com mensagem |
git push origin main | Envia commits para o remote |
git pull | Busca + merge do remote |
git log --oneline | Histórico compacto |
git diff | Diff do que ainda não foi staged |
git stash | Guarda mudanças temporariamente |
git reset --soft HEAD~1 | Desfaz último commit, mantém staging |
Branches & Fluxo de Trabalho
feat: nova funcionalidade, fix: correção de bug, docs: documentação, chore: manutenção, refactor: refatoração, test: testes. Muitas ferramentas de changelog automático dependem disso.
Merge vs Rebase
A---B---C main
\ \
D---E---M ← merge commit
A---B---C---D'---E' main ← commits reescritos no topo
.gitignore — O Essencial
# Dependências node_modules/ .pnp .pnp.js # Build outputs dist/ .next/ out/ build/ # Variáveis de ambiente .env .env.local .env.*.local # IDEs .vscode/settings.json .idea/ *.swp # OS .DS_Store Thumbs.db # Logs *.log npm-debug.log*
Node.js
Runtime JS no servidor. Instale via nvm (Node Version Manager) para gerenciar múltiplas versões.
npm / pnpm / yarn
Gerenciadores de pacotes. pnpm é o mais eficiente hoje em dia — economiza espaço em disco com links simbólicos.
VS Code
Editor padrão da indústria. Extensões essenciais: ESLint, Prettier, GitLens, Tailwind IntelliSense.
DevTools
F12 no browser. Aprenda: Elements, Console, Network (timing de requests), Sources (debugging) e Performance.
package.json — O Mapa do Projeto
{
"name": "meu-projeto",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx",
"format": "prettier --write ."
},
"dependencies": { // vai para produção
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": { // só no desenvolvimento
"vite": "^5.0.0",
"typescript": "^5.3.0",
"@types/react": "^18.3.0",
"eslint": "^8.57.0",
"prettier": "^3.2.0"
}
}
HTML Semântico
Usar as tags certas não é frescura — afeta SEO, acessibilidade (leitores de tela) e manutenibilidade.
<!-- ❌ Não semântico --> <div class="header"> <div class="nav"></div> </div> <div class="content"> <div class="article"></div> </div> <!-- ✅ Semântico --> <header> <nav></nav> </header> <main> <article> <h1>Título</h1> <section></section> <aside></aside> </article> </main> <footer></footer>
CSS Layout: Flexbox & Grid
/* Flexbox — 1D: linha OU coluna */ .flex-container { display: flex; align-items: center; /* eixo cruzado */ justify-content: space-between; /* eixo principal */ gap: 1rem; flex-wrap: wrap; } /* Grid — 2D: linhas E colunas */ .grid-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1.5rem; } /* CSS Custom Properties (variáveis) */ :root { --color-primary: #c94a1e; --spacing-md: 1rem; --radius: 6px; } .button { background: var(--color-primary); padding: var(--spacing-md); border-radius: var(--radius); } /* Media queries modernas */ @media (width >= 768px) { .container { max-width: 1200px; } } /* Container queries (CSS moderno!) */ .card-container { container-type: inline-size; } @container (min-width: 400px) { .card { display: grid; grid-template-columns: 1fr 2fr; } }
Tailwind não é CSS inline glorificado. É um sistema de design consistente onde você compõe classes utilitárias ao invés de escrever CSS custom. O resultado é mais rápido de escrever e mais fácil de manter.
Setup com Vite
import type { Config } from "tailwindcss"; export default { content: ["./index.html", "./src/**/*.{ts,tsx}"], theme: { extend: { colors: { brand: { "50": "#fef3ee", "500": "#c94a1e", "900": "#7c1d07", }, }, fontFamily: { sans: ["Outfit", "system-ui"], }, }, }, plugins: [], } satisfies Config;
Classes Fundamentais que Você Usará Todo Dia
| Categoria | Classes | Equivalente CSS |
|---|---|---|
| Display | flex grid hidden block | display: flex etc. |
| Flexbox | items-center justify-between gap-4 flex-wrap | align-items, justify-content... |
| Grid | grid-cols-3 col-span-2 gap-6 | grid-template-columns... |
| Spacing | p-4 px-6 mt-8 mx-auto | padding, margin (1 unit = 4px) |
| Texto | text-lg font-bold text-gray-600 leading-relaxed | font-size, font-weight... |
| Background | bg-white bg-brand-500 bg-gradient-to-r | background |
| Border | border border-gray-200 rounded-lg ring-2 | border, border-radius... |
| Shadow | shadow-sm shadow-lg shadow-xl | box-shadow |
| Responsive | md:flex lg:grid-cols-4 sm:text-base | Media queries |
| State | hover:bg-red-600 focus:ring disabled:opacity-50 | Pseudo-seletores |
| Dark mode | dark:bg-gray-900 dark:text-white | Media prefers-color-scheme |
// Componente de botão com Tailwind — pattern real import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const button = cva( "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50", { variants: { variant: { primary: "bg-brand-500 text-white hover:bg-brand-600 focus:ring-brand-500", secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400", ghost: "hover:bg-gray-100 focus:ring-gray-300", danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500", }, size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4 text-sm", lg: "h-12 px-6 text-base", }, }, defaultVariants: { variant: "primary", size: "md" }, } ); type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof button>; export function Button({ variant, size, className, ...props }: ButtonProps) { return <button className={cn(button({ variant, size }), className)} {...props} />; }
clsx + tailwind-merge e crie o utilitário: export const cn = (...inputs) => twMerge(clsx(inputs)). Ele resolve conflitos de classes Tailwind (ex: p-4 p-6 → vira p-6) e condiciona classes de forma limpa.
// Regras que NUNCA podem ser quebradas: // 1. Hooks só no top-level (não dentro de if/loops) // 2. Hooks só em componentes React ou custom hooks // 3. Nomes de custom hooks começam com "use" import { useState, useEffect, useCallback, useMemo, useRef } from "react"; // useState — estado local const [count, setCount] = useState(0); const [user, setUser] = useState<User | null>(null); // useEffect — efeitos colaterais useEffect(() => { // roda após render const sub = subscribe(userId); return () => sub.unsubscribe(); // cleanup }, [userId]); // dependency array // useCallback — memoiza funções (evita re-criação) const handleSubmit = useCallback((data: FormData) => { mutation.mutate(data); }, [mutation]); // useMemo — memoiza valores caros const sortedItems = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items] ); // useRef — referência mutável, não causa re-render const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current?.focus(); }, []);
Padrões de Composição
// Compound components — API fluente e flexível const Card = ({ children }: { children: ReactNode }) => <div className="rounded-xl border bg-white shadow-sm">{children}</div>; Card.Header = ({ children }: { children: ReactNode }) => <div className="border-b p-4 font-semibold">{children}</div>; Card.Body = ({ children }: { children: ReactNode }) => <div className="p-4">{children}</div>; Card.Footer = ({ children }: { children: ReactNode }) => <div className="border-t p-4">{children}</div>; // Uso: <Card> <Card.Header>Título</Card.Header> <Card.Body>Conteúdo</Card.Body> <Card.Footer>Rodapé</Card.Footer> </Card> // Render props / children como função function DataTable<T>({ data, renderRow }: { data: T[]; renderRow: (item: T, index: number) => ReactNode; }) { return <table><tbody>{data.map(renderRow)}</tbody></table>; }
Performance: Quando e Como Otimizar
React.memo, useMemo e useCallback têm custo. Só use quando o React DevTools Profiler mostrar re-renders desnecessários custosos. Otimização prematura cria código mais complexo sem benefício real.
// React.memo — evita re-render se props não mudaram const ExpensiveRow = React.memo(({ item }: { item: Item }) => { return <tr>...</tr>; }); // Lazy loading — code splitting automático const HeavyChart = React.lazy(() => import("./HeavyChart")); function Dashboard() { return ( <React.Suspense fallback={<Skeleton />}> <HeavyChart /> </React.Suspense> ); } // useTransition — marca update como não-urgente (React 18) const [isPending, startTransition] = useTransition(); startTransition(() => setSearchQuery(value)); // não bloqueia UI
| Biblioteca | Tipo | Quando usar |
|---|---|---|
| shadcn/ui | Headless + Radix + Tailwind | Projetos modernos. Código é seu — não é dependência |
| Radix UI | Headless (sem estilo) | Quando você quer total controle do estilo |
| Headless UI | Headless (Tailwind Labs) | Projetos Tailwind, componentes simples |
| Material UI (MUI) | Styled completo | Projetos corporativos com design system Material |
| Mantine | Styled completo | Produtividade máxima, muitos componentes prontos |
| Ant Design | Styled completo | Dashboards admin, mercado asiático/enterprise |
shadcn/ui — O Padrão Moderno
shadcn/ui não é uma biblioteca de pacotes — você copia os componentes para dentro do seu projeto. Isso significa controle total, sem ficar preso a versões externas.
// shadcn/ui — uso com Form (react-hook-form + zod) import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"; const schema = z.object({ email: z.string().email("Email inválido"), senha: z.string().min(8, "Mínimo 8 caracteres"), }); export function LoginForm() { const form = useForm({ resolver: zodResolver(schema) }); return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl><Input {...field} /></FormControl> <FormMessage /> {/* erro automático do zod */} </FormItem> )} /> </form> </Form> ); }
| Ferramenta | Para que serve | Use quando |
|---|---|---|
| TanStack Query | Server state (cache, refetch, loading) | SEMPRE que buscar dados de API |
| Zustand | Client state global simples | Estado compartilhado entre componentes distantes |
| Jotai | Atomic state (bottom-up) | Estado granular, derivado de átomos |
| Redux Toolkit | State + DevTools + RTK Query | Apps complexas, time grande, muito state |
| Context API | Injeção de dependência | Estado que raramente muda (tema, auth, config) |
TanStack Query — O Padrão para Server State
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; // Setup — uma vez na raiz da app const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, retry: 2 }, }, }); // Query — busca e cacheia dados function useUsers(page: number) { return useQuery({ queryKey: ["users", page], queryFn: () => fetchUsers(page), placeholderData: keepPreviousData, // evita flash ao paginar }); } // Mutation — cria/atualiza/deleta + invalida cache function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: CreateUserDto) => api.post("/users", data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); }, onError: (err) => toast.error(err.message), }); } // No componente: const { data, isLoading, isError, error } = useUsers(currentPage); const createUser = useCreateUser();
Zustand — Estado Global Simples
import { create } from "zustand"; import { persist } from "zustand/middleware"; type CartStore = { items: CartItem[]; total: number; addItem: (item: CartItem) => void; removeItem: (id: string) => void; clear: () => void; }; export const useCartStore = create<CartStore>()( persist( (set, get) => ({ items: [], total: 0, addItem(item) { set(s => ({ items: [...s.items, item], total: s.total + item.price, })); }, removeItem(id) { set(s => { const item = s.items.find(i => i.id === id); return { items: s.items.filter(i => i.id !== id), total: s.total - (item?.price ?? 0), }; }); }, clear() { set({ items: [], total: 0 }); }, }), { name: "cart" } // persiste no localStorage ) );
Vite é o bundler padrão para SPAs modernas. Usa ESModules nativos em dev (sem bundle = startup instantâneo) e Rollup em produção (bundle otimizado).
vite.config.ts — Configuração Completa
import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; export default defineConfig({ plugins: [react()], resolve: { alias: { "@": path.resolve(__dirname, "./src") }, }, server: { port: 5173, proxy: { "/api": { target: "http://localhost:3001", changeOrigin: true, }, }, }, build: { outDir: "dist", sourcemap: true, rollupOptions: { output: { manualChunks: { vendor: ["react", "react-dom"], ui: ["@radix-ui/react-dialog", "lucide-react"], query: ["@tanstack/react-query"], }, }, }, }, });
ESLint + Prettier — Qualidade de Código
import js from "@eslint/js"; import ts from "typescript-eslint"; import reactHooks from "eslint-plugin-react-hooks"; export default ts.config( js.configs.recommended, ...ts.configs.recommended, { plugins: { "react-hooks": reactHooks }, rules: { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], }, } );
Next.js é o framework React mais usado em produção. O App Router (Next 13+) unifica SSR, SSG, ISR e Server Components em um único modelo mental baseado em arquivos.
Estrutura de Pastas
app/
├── layout.tsx ← root layout (html, body, providers)
├── page.tsx ← rota /
├── loading.tsx ← Suspense fallback automático
├── error.tsx ← Error Boundary automático
├── not-found.tsx ← página 404
├── globals.css
│
├── (auth)/ ← route group (não aparece na URL)
│ ├── login/page.tsx ← rota /login
│ └── signup/page.tsx ← rota /signup
│
├── dashboard/
│ ├── layout.tsx ← layout do dashboard
│ ├── page.tsx ← rota /dashboard
│ └── [id]/
│ └── page.tsx ← rota /dashboard/:id
│
└── api/
└── users/
└── route.ts ← GET /api/users, POST /api/users
Server vs Client Components
| Server Component (padrão) | Client Component ('use client') | |
|---|---|---|
| Onde roda | Servidor (Node.js) | Browser (+ hidratação) |
| Acesso a DB | ✅ Direto | ❌ Via API |
| useState/hooks | ❌ | ✅ |
| Event handlers | ❌ | ✅ |
| Bundle JS | 0kb no cliente | Incluído no bundle |
| SEO | ✅ HTML completo | ⚠️ Depende |
// Server Component — async, busca dado direto import { db } from "@/lib/db"; import { PostCard } from "./PostCard"; export default async function PostsPage() { const posts = await db.post.findMany({ where: { published: true }, orderBy: { createdAt: "desc" }, take: 20, }); return ( <main className="container mx-auto py-8"> <h1 className="text-3xl font-bold mb-6">Posts</h1> <div className="grid grid-cols-3 gap-6"> {posts.map(post => <PostCard key={post.id} post={post} />)} </div> </main> ); } // PostCard.tsx — Client Component (tem interatividade) "use client"; export function PostCard({ post }: { post: Post }) { const [liked, setLiked] = useState(false); return ( <article> <h2>{post.title}</h2> <button onClick={() => setLiked(l => !l)}> {liked ? "❤️" : "🤍"} </button> </article> ); }
Caching no Next.js 15
// fetch com opções de cache // Static (SSG) — cacheia para sempre fetch("/api/config", { cache: "force-cache" }); // Dynamic (SSR) — nunca cacheia fetch("/api/live", { cache: "no-store" }); // ISR — revalida a cada N segundos fetch("/api/posts", { next: { revalidate: 60 } }); // Tags para revalidação sob demanda fetch("/api/posts", { next: { tags: ["posts"] } }); // Em server action ou API route: import { revalidateTag } from "next/cache"; revalidateTag("posts"); // invalida todas as fetches com tag "posts"
Para APIs standalone (sem Next.js), Fastify é a escolha moderna — mais rápido que Express, TypeScript nativo, schema validation integrado. Para projetos simples ou legado, Express ainda é válido.
API REST com Fastify + Zod
import Fastify from "fastify"; import { z } from "zod"; import { db } from "./db"; const app = Fastify({ logger: true }); // Plugin de CORS await app.register(import("@fastify/cors"), { origin: ["http://localhost:5173"], }); // Schema Zod → Fastify const CreatePostBody = z.object({ title: z.string().min(1).max(200), content: z.string().min(10), tags: z.array(z.string()).optional(), }); // GET /posts app.get("/posts", async (req, reply) => { const { page = 1, limit = 10 } = req.query as { page?: number; limit?: number }; const [posts, total] = await Promise.all([ db.post.findMany({ skip: (page - 1) * limit, take: limit }), db.post.count(), ]); return { posts, total, page, pages: Math.ceil(total / limit) }; }); // POST /posts app.post("/posts", async (req, reply) => { const parsed = CreatePostBody.safeParse(req.body); if (!parsed.success) { return reply.status(422).send({ errors: parsed.error.flatten() }); } const post = await db.post.create({ data: parsed.data }); return reply.status(201).send(post); }); await app.listen({ port: 3001 });
Boas Práticas de API Design
| Prática | Errado ❌ | Certo ✅ |
|---|---|---|
| Verbos HTTP | POST /getUser | GET /users/:id |
| Nomes de recursos | /getUsers /createPost | /users /posts (substantivos) |
| Plural | /user /post | /users /posts |
| Nested resources | /getPosts?userId=1 | /users/1/posts |
| Erros | { success: false, msg: "err" } | { error: { code, message, details } } |
| Paginação | Retornar tudo | { data, total, page, limit, pages } |
| Versioning | Quebrar clientes | /api/v1/users → /api/v2/users |
PostgreSQL
Banco relacional padrão para aplicações sérias. ACID compliant, JSON nativo, full-text search.
Prisma ORM
ORM TypeScript-first com schema declarativo e client 100% tipado. Padrão do ecossistema Next.js.
Drizzle ORM
ORM SQL-like, mais leve que Prisma. Melhor performance e controle granular de queries.
MongoDB
Banco de documentos. Bom para schemas flexíveis, mas cuidado com consistência em dados relacionais.
Prisma — Setup e Operações
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String role Role @default(USER) posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") // nome na tabela do banco } model Post { id String @id @default(cuid()) title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String tags Tag[] createdAt DateTime @default(now()) @@index([authorId]) @@map("posts") } enum Role { USER ADMIN MODERATOR }
import { db } from "./db"; // SELECT com relacionamento const postsComAutor = await db.post.findMany({ where: { published: true, author: { role: "ADMIN" } }, include: { author: { select: { name: true, email: true } } }, orderBy: { createdAt: "desc" }, take: 10, skip: 0, }); // UPDATE com where const updated = await db.post.update({ where: { id: postId, authorId: userId }, // segurança data: { title: newTitle, updatedAt: new Date() }, }); // Transaction — atomicidade garantida const [user, post] = await db.$transaction([ db.user.create({ data: userData }), db.post.create({ data: { ...postData, authorId: userId } }), ]); // Aggregate const stats = await db.order.aggregate({ _sum: { total: true }, _avg: { total: true }, _count: true, where: { createdAt: { gte: startOfMonth } }, });
| Estratégia | Como funciona | Melhor para |
|---|---|---|
| JWT (Stateless) | Token assinado armazenado no cliente. Servidor valida assinatura sem consultar banco. | APIs, microserviços, múltiplos servidores |
| Session (Stateful) | ID de sessão no cookie. Estado armazenado no servidor (Redis/DB). | Monolitos, quando precisa invalidar sessões |
| OAuth / OIDC | Delegação de autenticação a terceiros (Google, GitHub, etc.) | Login social, SSO corporativo |
Auth.js (Next-Auth v5) — Implementação
import NextAuth from "next-auth"; import GitHub from "next-auth/providers/github"; import Credentials from "next-auth/providers/credentials"; import { db } from "./lib/db"; import bcrypt from "bcryptjs"; export const { auth, handlers, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }), Credentials({ async authorize(credentials) { const user = await db.user.findUnique({ where: { email: credentials.email as string } }); if (!user?.password) return null; const ok = await bcrypt.compare(credentials.password as string, user.password); return ok ? user : null; }, }), ], callbacks: { async session({ session, token }) { session.user.id = token.sub!; session.user.role = token.role as string; return session; }, async jwt({ token, user }) { if (user) token.role = (user as User).role; return token; }, }, });
Segurança: O Checklist
- Nunca armazene senhas em texto plano — sempre bcrypt/argon2
- Nunca confie em dados do cliente sem validação no servidor
- Nunca exponha stack traces em produção
- Nunca use
eval()com dados do usuário - Nunca construa SQL com concatenação de strings (SQL injection)
| Vulnerabilidade | Prevenção |
|---|---|
| SQL Injection | Use ORM (Prisma) ou prepared statements. Nunca concatene input do usuário em queries. |
| XSS | React escapa HTML por padrão. Nunca use dangerouslySetInnerHTML com input do usuário. |
| CSRF | CORS correto, SameSite cookies, tokens CSRF para forms. |
| Rate Limiting | Limite requests por IP/usuário com @upstash/ratelimit ou similar. |
| Secrets expostos | Use .env + servidor de secrets (AWS SSM, Vault). Nunca commite .env. |
| IDOR | Sempre verifique que o usuário autenticado tem acesso ao recurso requisitado. |
Plataformas por Caso de Uso
Vercel
Melhor para Next.js. Deploy via Git, edge functions, preview por PR. Free tier generoso.
Railway
Melhor para backends Node.js + banco de dados. Deploy de Docker ou via Git. Preço por uso.
Render
Alternativa ao Heroku. Web services, workers, cron jobs, banco PostgreSQL managed.
AWS / GCP / Azure
Para escala enterprise. Mais complexo mas máximo controle e infraestrutura.
Docker — Container do Backend
# Multi-stage build — imagem final menor FROM node:20-alpine AS base RUN corepack enable pnpm # Stage 1: instalar dependências FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile # Stage 2: build FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm build # Stage 3: produção — imagem mínima FROM base AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/dist ./dist COPY --from=deps /app/node_modules ./node_modules EXPOSE 3001 CMD ["node", "dist/server.js"]
GitHub Actions — CI/CD
name: CI/CD on: push: { branches: [main] } pull_request: { branches: [main] } jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 - uses: actions/setup-node@v4 with: { node-version: 20, cache: pnpm } - name: Install run: pnpm install --frozen-lockfile - name: Type check run: pnpm tsc --noEmit - name: Lint run: pnpm lint - name: Test run: pnpm test - name: Build run: pnpm build deploy: needs: check if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Deploy to Railway run: railway up env: { RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} }
Estrutura de Pastas para Projetos Grandes
src/ ├── app/ ← Next.js App Router │ ├── components/ │ ├── ui/ ← shadcn/ui (primitivos) │ ├── common/ ← compartilhados entre features │ └── layouts/ ← Shell, Sidebar, Header │ ├── features/ ← organizado por domínio de negócio │ ├── auth/ │ │ ├── components/ ← LoginForm, SignupModal │ │ ├── hooks/ ← useAuth, useSession │ │ ├── actions.ts ← Server Actions │ │ └── schema.ts ← Zod schemas │ ├── posts/ │ │ ├── components/ │ │ ├── hooks/ │ │ ├── api.ts ← fetch functions │ │ └── types.ts │ └── dashboard/ │ ├── lib/ │ ├── db.ts ← Prisma client (singleton) │ ├── auth.ts ← Auth.js config │ ├── utils.ts ← cn(), formatDate(), etc. │ └── validators.ts ← schemas Zod reutilizáveis │ ├── hooks/ ← hooks globais (useDebounce, useLocalStorage) ├── store/ ← Zustand stores globais └── types/ ← tipos compartilhados + .d.ts
features/posts/.
O Stack Recomendado em 2025
| Camada | Ferramenta | Alternativa |
|---|---|---|
| Framework | Next.js 15 | Remix, Astro, Nuxt (Vue) |
| Linguagem | TypeScript 5 | — |
| Estilo | Tailwind CSS 4 | CSS Modules, Styled Components |
| UI Components | shadcn/ui + Radix | Mantine, MUI |
| Forms | React Hook Form + Zod | Formik, Conform |
| Server State | TanStack Query | SWR, RTK Query |
| Client State | Zustand | Jotai, Redux Toolkit |
| Auth | Auth.js v5 | Clerk, Lucia, Better Auth |
| ORM | Prisma / Drizzle | Kysely, TypeORM |
| Banco | PostgreSQL | MySQL, SQLite (dev) |
| Deploy Frontend | Vercel | Netlify, Cloudflare Pages |
| Deploy Backend | Railway / Render | Fly.io, AWS ECS |
| Package Manager | pnpm | Bun, npm, Yarn |
| Testes | Vitest + Testing Library | Jest, Playwright |
Próximos Passos para Avançar
Testes
Vitest para unitários, Testing Library para componentes, Playwright para e2e. A pirâmide de testes aplicada a React.
Observabilidade
Sentry para errors, Datadog/Grafana para métricas, OpenTelemetry para tracing distribuído.
Performance
Core Web Vitals (LCP, CLS, FID), Lighthouse, Web Workers, streaming com Suspense.
Monorepos
Turborepo + pnpm workspaces para múltiplas apps compartilhando packages. Escala para times grandes.