O que este projeto cobre (e o que fica fora)
Neste capítulo, o objetivo é definir com precisão o escopo do projeto que será construído ao longo do curso: uma SPA (Single Page Application) em React com rotas públicas e privadas, autenticação baseada em JWT, e práticas mínimas de segurança e organização para que o app seja sustentável.
Definir escopo evita dois problemas comuns: (1) tentar “fazer tudo” e acabar com uma base confusa; (2) construir algo que funciona apenas no cenário ideal e quebra em situações reais (refresh da página, token expirado, navegação direta por URL, etc.).
O projeto terá como foco o lado do cliente (React) e a integração com uma API que emite tokens. A API pode ser um backend real (Node, Java, .NET, etc.) ou um mock local, desde que respeite o contrato de autenticação. O importante é que o front-end trate autenticação e roteamento de forma consistente.
Funcionalidades incluídas no escopo
- Rotas públicas e privadas: páginas acessíveis sem login (ex.: Login) e páginas protegidas (ex.: Dashboard).
- Autenticação com JWT: login obtém um token; o token é usado para autorizar chamadas subsequentes.
- Persistência de sessão: manter o usuário autenticado após refresh, respeitando expiração e logout.
- Guarda de rota (route guard): impedir acesso a rotas privadas quando não autenticado e redirecionar adequadamente.
- Redirecionamento pós-login: se o usuário tentou acessar uma rota privada, após autenticar ele volta para a rota desejada.
- Camada de API centralizada: um módulo para chamadas HTTP com inclusão automática do token e tratamento de erros (ex.: 401).
- Tratamento de token expirado: ao receber 401/403, limpar sessão e enviar para login.
- Estrutura de pastas e responsabilidades: separar páginas, componentes, serviços e contexto/estado de autenticação.
Fora do escopo (para não desviar do objetivo)
- Autorização avançada por papéis/permissões (RBAC/ABAC) além do básico. Podemos mencionar como extensão, mas não implementar como requisito central.
- Refresh token completo com rotação e revogação no backend. O front-end será preparado para lidar com expiração e, opcionalmente, com um endpoint de refresh, mas não será obrigatório.
- SSR/Next.js: o foco é SPA com React Router.
- Criptografia no front-end: não faz sentido “criptografar” token no cliente como segurança real; o foco é armazenamento e fluxo corretos.
- Testes automatizados completos: podem ser adicionados depois; aqui priorizamos o fluxo funcional e a arquitetura.
Pré-requisitos práticos (o que você precisa ter pronto)
Para acompanhar o projeto, você precisa de um ambiente funcional para rodar React, instalar dependências e simular uma API. A lista abaixo é prática e objetiva: se você cumprir estes itens, conseguirá executar o passo a passo sem travar em detalhes de ambiente.
Ferramentas necessárias
- Node.js LTS instalado (com npm). Alternativamente, pode usar pnpm ou yarn, mas os comandos aqui usarão npm.
- Editor (VS Code ou equivalente) com suporte a ESLint/Prettier (opcional, mas recomendado).
- Git (opcional, mas útil para checkpoints).
- Navegador com DevTools para inspecionar Network, Local Storage e Console.
Conhecimentos mínimos esperados
- JS/TS básico: funções, async/await, módulos.
- React básico: componentes, hooks (useState/useEffect), props.
- HTTP básico: request/response, headers, status codes (200/401/403).
Contrato mínimo da API de autenticação
Mesmo que você use um mock, defina um contrato. Um contrato simples e suficiente para o projeto:
- POST /auth/login recebe credenciais e retorna um JWT (e opcionalmente dados do usuário).
- GET /me retorna dados do usuário autenticado (exige Authorization: Bearer <token>).
Exemplo de resposta de login (modelo):
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "123", "name": "Ana", "email": "ana@exemplo.com" }}Exemplo de chamada autenticada:
GET /meAuthorization: Bearer eyJhbGciOi...Se a API retornar 401 (não autenticado) ou 403 (sem permissão), o front-end deve reagir limpando a sessão e redirecionando para login (no escopo deste projeto, trataremos 401 como “sessão inválida/expirada”).
Decisões de arquitetura (para evitar retrabalho)
Antes de codar, vale fixar algumas decisões. Elas determinam como o roteamento e a autenticação se encaixam e evitam “gambiarras” como verificar token em toda página manualmente.
Onde o estado de autenticação vive
Você precisa de um lugar central para: (1) guardar o token; (2) expor se o usuário está autenticado; (3) executar login/logout; (4) carregar o usuário ao iniciar o app. Para isso, uma abordagem simples é usar um AuthContext com um hook useAuth.
- AuthProvider: componente no topo da árvore que mantém estado e funções.
- useAuth: hook para consumir o contexto.
Onde o token é armazenado
Para uma SPA, as opções mais comuns são:
- Memory: mais seguro contra XSS persistente, mas perde no refresh.
- localStorage: persiste no refresh, mas é acessível via JS (risco maior em caso de XSS).
- Cookies httpOnly: mais seguro contra acesso via JS, mas exige suporte do backend e cuidados com CSRF.
Como pré-requisito prático e para manter o projeto executável com qualquer API simples, este projeto assume localStorage para persistência do token, com a recomendação explícita de que, em produção, cookies httpOnly podem ser preferíveis dependendo do cenário. O importante aqui é implementar o fluxo corretamente e reduzir superfícies de erro (ex.: não esquecer de limpar token no logout).
Como proteger rotas
Em vez de condicionar renderização dentro de cada página, criaremos um componente de proteção de rota, por exemplo ProtectedRoute, que decide entre renderizar o conteúdo ou redirecionar para login. Isso centraliza a regra e evita duplicação.
Como lidar com “carregando” na inicialização
Ao abrir o app, pode existir um token no storage, mas você ainda não sabe se ele é válido até consultar a API (ex.: /me). Portanto, o AuthProvider precisa de um estado de loading para evitar piscar a UI (mostrar rapidamente uma rota privada e depois expulsar). O fluxo típico:
- App inicia
- AuthProvider lê token do storage
- Se existe token, chama /me
- Se ok, autentica e libera rotas privadas
- Se falhar, limpa token e manda para login
Estrutura sugerida de pastas
Uma estrutura simples, mas escalável, para o projeto:
src/ api/ httpClient.js authApi.js auth/ AuthContext.jsx useAuth.js ProtectedRoute.jsx pages/ Login.jsx Dashboard.jsx Settings.jsx components/ AppLayout.jsx routes/ AppRoutes.jsx main.jsx App.jsxRegras práticas:
- pages/: componentes de página (ligados a rotas).
- auth/: tudo que é autenticação (contexto, hooks, guard).
- api/: cliente HTTP e funções de acesso à API.
- routes/: definição das rotas (React Router) separada do resto.
Passo a passo prático: criando a base do projeto
A seguir, um passo a passo para criar a base mínima do app com React Router e o esqueleto de autenticação. Ajuste nomes conforme sua preferência, mas mantenha as responsabilidades.
1) Criar o projeto React
Você pode usar Vite para uma configuração rápida:
npm create vite@latest spa-auth -- --template reactcd spa-authnpm installInicie o servidor:
npm run dev2) Instalar dependências de roteamento e HTTP
Instale React Router e um cliente HTTP (axios é opcional; fetch também funciona). Para manter o exemplo direto, usaremos axios:
npm install react-router-dom axios3) Criar um httpClient centralizado
Crie src/api/httpClient.js. A ideia é: (1) configurar baseURL; (2) anexar token automaticamente; (3) interceptar 401 para forçar logout.
import axios from "axios";export const httpClient = axios.create({ baseURL: "http://localhost:3000",});httpClient.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});httpClient.interceptors.response.use( (response) => response, (error) => { if (error?.response?.status === 401) { localStorage.removeItem("token"); } return Promise.reject(error); }Observação prática: remover token no interceptor é útil, mas o redirecionamento para login deve ser coordenado pelo AuthProvider/roteamento. O interceptor apenas garante que um token inválido não fique “preso” no storage.
4) Criar funções de API de autenticação
Crie src/api/authApi.js:
import { httpClient } from "./httpClient";export async function loginRequest(email, password) { const { data } = await httpClient.post("/auth/login", { email, password }); return data;}export async function meRequest() { const { data } = await httpClient.get("/me"); return data;}5) Implementar AuthContext com login, logout e bootstrap
Crie src/auth/AuthContext.jsx. Aqui ficam as regras de sessão:
import { createContext, useEffect, useMemo, useState } from "react";import { loginRequest, meRequest } from "../api/authApi";export const AuthContext = createContext(null);export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const isAuthenticated = !!user; async function login(email, password) { const data = await loginRequest(email, password); localStorage.setItem("token", data.token); setUser(data.user ?? null); if (!data.user) { const me = await meRequest(); setUser(me); } } function logout() { localStorage.removeItem("token"); setUser(null); } useEffect(() => { let isMounted = true; async function bootstrap() { const token = localStorage.getItem("token"); if (!token) { if (isMounted) { setUser(null); setIsLoading(false); } return; } try { const me = await meRequest(); if (isMounted) setUser(me); } catch (e) { localStorage.removeItem("token"); if (isMounted) setUser(null); } finally { if (isMounted) setIsLoading(false); } } bootstrap(); return () => { isMounted = false; }; }, []); const value = useMemo( () => ({ user, isAuthenticated, isLoading, login, logout }), [user, isAuthenticated, isLoading] ); return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;}Pontos importantes desse bootstrap:
- Evita “piscar” conteúdo protegido enquanto valida token.
- Se /me falhar, remove token e limpa usuário.
- Permite que o backend retorne user no login ou não; o front se adapta.
6) Criar o hook useAuth
Crie src/auth/useAuth.js:
import { useContext } from "react";import { AuthContext } from "./AuthContext";export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth deve ser usado dentro de AuthProvider"); return ctx;}7) Criar o componente ProtectedRoute
Crie src/auth/ProtectedRoute.jsx. Ele decide se renderiza a rota privada ou redireciona para login, e preserva a rota de origem para redirecionamento pós-login.
import { Navigate, Outlet, useLocation } from "react-router-dom";import { useAuth } from "./useAuth";export function ProtectedRoute() { const { isAuthenticated, isLoading } = useAuth(); const location = useLocation(); if (isLoading) { return <p>Carregando...</p>; } if (!isAuthenticated) { return <Navigate to="/login" replace state={{ from: location }} />; } return <Outlet />;}O Outlet permite proteger um grupo de rotas aninhadas, mantendo o roteamento limpo.
8) Criar páginas mínimas: Login e Dashboard
Crie src/pages/Login.jsx:
import { useState } from "react";import { useLocation, useNavigate } from "react-router-dom";import { useAuth } from "../auth/useAuth";export default function Login() { const { login } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const from = location.state?.from?.pathname || "/"; const [email, setEmail] = useState("ana@exemplo.com"); const [password, setPassword] = useState("123456"); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); async function handleSubmit(e) { e.preventDefault(); setError(null); setSubmitting(true); try { await login(email, password); navigate(from, { replace: true }); } catch (e) { setError("Credenciais inválidas ou erro de rede"); } finally { setSubmitting(false); } } return ( <div> <h2>Login</h2> <form onSubmit={handleSubmit}> <div> <label>Email</label> <input value={email} onChange={(e) => setEmail(e.target.value)} /> </div> <div> <label>Senha</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> {error && <p>{error}</p>} <button disabled={submitting} type="submit"> {submitting ? "Entrando..." : "Entrar"} </button> </form> </div> );}Crie src/pages/Dashboard.jsx:
import { useAuth } from "../auth/useAuth";export default function Dashboard() { const { user, logout } = useAuth(); return ( <div> <h2>Dashboard</h2> <p>Bem-vindo{user?.name ? `, ${user.name}` : ""}.</p> <button onClick={logout}>Sair</button> </div> );}9) Definir rotas com React Router
Crie src/routes/AppRoutes.jsx:
import { BrowserRouter, Route, Routes } from "react-router-dom";import { ProtectedRoute } from "../auth/ProtectedRoute";import Login from "../pages/Login";import Dashboard from "../pages/Dashboard";export function AppRoutes() { return ( <BrowserRouter> <Routes> <Route path="/login" element={<Login />} /> <Route element={<ProtectedRoute />}> <Route path="/" element={<Dashboard />} /> </Route> <Route path="*" element={<p>Página não encontrada</p>} /> </Routes> </BrowserRouter> );}Esse desenho já cobre: rota pública (/login), rota privada (/), e fallback para rotas inexistentes.
10) Envolver a aplicação com AuthProvider
Atualize src/main.jsx para incluir o provider:
import React from "react";import ReactDOM from "react-dom/client";import { AuthProvider } from "./auth/AuthContext";import { AppRoutes } from "./routes/AppRoutes";ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <AuthProvider> <AppRoutes /> </AuthProvider> </React.StrictMode>);Checklist de validação do escopo (para saber se está pronto)
Antes de avançar para capítulos mais específicos, valide estes comportamentos. Eles são o “mínimo funcional” para uma SPA com rotas e autenticação:
- Acessar “/” sem token redireciona para /login.
- Após login, o token é salvo e o usuário é redirecionado para a rota original (se houver).
- Refresh na rota privada mantém a sessão se o token for válido (bootstrap chama /me).
- Token inválido/expirado faz /me falhar, limpa token e volta ao login.
- Logout remove token e impede acesso a rotas privadas.
Pré-requisitos de segurança e boas práticas (aplicáveis já no início)
Mesmo em um projeto didático, algumas práticas evitam problemas reais e ajudam a manter o app previsível.
Não confiar no front-end para segurança
Rotas privadas no React servem para experiência do usuário e organização. A segurança real está no backend validando o token em cada endpoint protegido. Portanto:
- Não trate “esconder página” como proteção de dados.
- Garanta que toda chamada sensível passe por endpoints que exigem autenticação.
Evitar espalhar acesso ao localStorage
Um erro comum é ler/escrever token em vários lugares. Centralize isso no AuthProvider e no httpClient. Assim, se você mudar a estratégia (ex.: cookies), o impacto é menor.
Tratar 401 de forma consistente
Quando a API responder 401, o app deve entrar em um estado coerente: token removido, usuário nulo, e navegação para login. Evite “meio logado”, onde o token existe mas o usuário não, ou vice-versa.
Separar autenticação de layout
Mesmo que você tenha um layout com menu e cabeçalho, não misture regras de autenticação com componentes visuais. O guard de rota e o provider devem decidir acesso; o layout apenas renderiza UI.
Como simular a API rapidamente (opção prática)
Se você ainda não tem backend, pode usar um mock simples para destravar o desenvolvimento. Uma opção é criar um servidor local com endpoints /auth/login e /me. O importante é respeitar status codes e headers. Abaixo, um exemplo conceitual do comportamento esperado:
- /auth/login: se email/senha forem aceitos, retorna token e user; se não, retorna 401.
- /me: se Authorization estiver ausente ou inválido, retorna 401; se válido, retorna user.
Mesmo usando mock, mantenha a disciplina de sempre validar o token no endpoint /me, porque isso força o front-end a lidar com o cenário real de expiração/invalidade.