Neste mini-projeto guiado, você vai montar a estrutura base do app e colocar o roteamento “de ponta a ponta” funcionando para três telas essenciais: Login (área pública), Dashboard (área autenticada) e Perfil (área autenticada). A ideia é sair com um esqueleto navegável, com páginas reais, navegação consistente e pontos de extensão claros para integrar APIs e regras de acesso mais avançadas depois.

Como capítulos anteriores já cobriram conceitos como rotas públicas/privadas, guards, Context de autenticação, persistência de sessão, interceptadores e refresh token, aqui o foco será: organizar o app para crescer, criar as páginas, conectar o roteamento e validar o fluxo de navegação com uma base limpa.
Objetivo do mini-projeto
Ao final, você terá:
- Uma estrutura mínima de pastas para páginas e componentes compartilhados.
- Rotas para
/login,/app(Dashboard) e/app/profile(Perfil). - Um layout simples para a área autenticada com navegação entre Dashboard e Perfil.
- Um fluxo de login “fake” (mock) para testar navegação e UI sem depender de backend.
- Um ponto único para evoluir depois para login real com JWT e chamadas HTTP.
Passo 1: criar o projeto e instalar dependências
Se você já tem o projeto criado, pule para o próximo passo. Caso contrário, crie um app React e instale o React Router.
# Exemplo com Vite (recomendado pela simplicidade e velocidade) npm create vite@latest spa-auth-router -- --template react cd spa-auth-router npm install npm install react-router-domEstrutura inicial esperada (simplificada):
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
spa-auth-router/ src/ main.jsx App.jsx package.jsonPasso 2: definir uma estrutura base de pastas (mínima e prática)
Você não precisa começar com uma arquitetura complexa, mas é importante separar páginas, componentes e infra (serviços/utilitários). Uma sugestão enxuta para este mini-projeto:

src/ app/ router/ AppRouter.jsx layout/ AppLayout.jsx pages/ DashboardPage.jsx ProfilePage.jsx auth/ pages/ LoginPage.jsx services/ authService.js shared/ components/ NavBar.jsx ProtectedRoute.jsx App.jsx main.jsxO que cada parte faz, na prática:
- auth/pages: telas públicas relacionadas a autenticação (aqui, Login).
- app/pages: telas da área autenticada (Dashboard e Perfil).
- app/layout: layout comum da área autenticada (menu, container, etc.).
- app/router: roteador do app (onde as rotas são declaradas).
- auth/services: serviço que simula login/logout (depois vira integração real).
- shared/components: componentes reutilizáveis (NavBar, ProtectedRoute).
Passo 3: preparar o ponto de entrada (main.jsx) com BrowserRouter
No React Router, o roteamento precisa estar “ligado” no topo da árvore. No Vite, isso costuma ficar em main.jsx.
import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> );Isso garante que qualquer componente abaixo possa usar Link, useNavigate, useLocation e as rotas declaradas.
Passo 4: criar o App.jsx como casca do roteador
O App aqui será bem simples: ele apenas renderiza o roteador do projeto.
import AppRouter from "./app/router/AppRouter.jsx"; export default function App() { return <AppRouter />; }Passo 5: criar um serviço de autenticação “mock” para testar o fluxo
Para este mini-projeto, você precisa de um jeito rápido de simular login/logout e um “usuário atual”. Em vez de integrar backend agora, crie um authService simples com armazenamento local. Isso permite validar navegação e comportamento das páginas.
// src/auth/services/authService.js const STORAGE_KEY = "demo_auth"; export function getSession() { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } export function login({ email, password }) { if (!email || !password) { throw new Error("Informe email e senha"); } const session = { user: { id: "u1", name: "Usuário Demo", email }, token: "demo-token" }; localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); return session; } export function logout() { localStorage.removeItem(STORAGE_KEY); }Observação importante: este token é fictício. O objetivo é apenas permitir que o app se comporte como se estivesse autenticado, para você construir a navegação e a estrutura das páginas.
Passo 6: criar um ProtectedRoute simples (guarda de rota)
Como você precisa proteger /app e /app/profile, crie um componente de guarda que verifica se existe sessão. Se não existir, redireciona para /login.
// src/shared/components/ProtectedRoute.jsx import { Navigate, Outlet, useLocation } from "react-router-dom"; import { getSession } from "../../auth/services/authService"; export default function ProtectedRoute() { const location = useLocation(); const session = getSession(); if (!session?.token) { return <Navigate to="/login" replace state={{ from: location }} />; } return <Outlet />; }O Outlet é o “buraco” onde as rotas filhas serão renderizadas. O state com from guarda a rota atual para você usar depois no login e voltar ao destino pretendido (mesmo que você refine isso mais tarde).
Passo 7: criar o layout da área autenticada (AppLayout)
O layout da área autenticada deve fornecer consistência: um cabeçalho, um menu e um container para as páginas internas. Ele também é um bom lugar para colocar o botão de logout.
// src/app/layout/AppLayout.jsx import { Outlet, useNavigate } from "react-router-dom"; import NavBar from "../../shared/components/NavBar.jsx"; import { logout } from "../../auth/services/authService"; export default function AppLayout() { const navigate = useNavigate(); function handleLogout() { logout(); navigate("/login", { replace: true }); } return ( <div style={{ fontFamily: "sans-serif" }}> <header style={{ padding: 16, borderBottom: "1px solid #ddd" }}> <h3 style={{ margin: 0 }}>Área autenticada</h3> </header> <NavBar onLogout={handleLogout} /> <main style={{ padding: 16 }}> <Outlet /> </main> </div> ); }Repare que o layout não conhece detalhes das páginas. Ele apenas oferece a moldura e renderiza o conteúdo via Outlet.
Passo 8: criar a NavBar com links para Dashboard e Perfil
Uma navegação simples ajuda a validar rapidamente se suas rotas estão corretas. Use NavLink para indicar o item ativo.
// src/shared/components/NavBar.jsx import { NavLink } from "react-router-dom"; export default function NavBar({ onLogout }) { const linkStyle = ({ isActive }) => ({ marginRight: 12, textDecoration: "none", fontWeight: isActive ? 700 : 400 }); return ( <nav style={{ padding: 16, borderBottom: "1px solid #eee" }}> <NavLink to="/app" style={linkStyle} end>Dashboard</NavLink> <NavLink to="/app/profile" style={linkStyle}>Perfil</NavLink> <button onClick={onLogout} style={{ marginLeft: 16 }}>Sair</button> </nav> ); }O atributo end no link do Dashboard evita que ele fique ativo também em /app/profile.
Passo 9: criar as páginas (Login, Dashboard, Perfil)
9.1 LoginPage
A página de login deve permitir inserir credenciais e, ao autenticar, redirecionar para a rota pretendida (se existir) ou para o Dashboard. Como o serviço é mock, o login será imediato.
// src/auth/pages/LoginPage.jsx import { useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { login, getSession } from "../services/authService"; export default function LoginPage() { const navigate = useNavigate(); const location = useLocation(); const [email, setEmail] = useState("demo@site.com"); const [password, setPassword] = useState("123456"); const [error, setError] = useState(""); const from = useMemo(() => { return location.state?.from?.pathname || "/app"; }, [location.state]); function handleSubmit(e) { e.preventDefault(); setError(""); try { login({ email, password }); navigate(from, { replace: true }); } catch (err) { setError(err.message || "Falha no login"); } } const session = getSession(); return ( <div style={{ maxWidth: 420, margin: "40px auto", fontFamily: "sans-serif" }}> <h3>Login</h3> <p>Use qualquer email e senha para entrar (modo demo).</p> {session?.token ? ( <p>Você já está autenticado. Vá para <a href="/app">/app</a>.</p> ) : null} <form onSubmit={handleSubmit}> <div style={{ marginBottom: 12 }}> <label>Email</label> <input value={email} onChange={(e) => setEmail(e.target.value)} style={{ width: "100%", padding: 8 }} /> </div> <div style={{ marginBottom: 12 }}> <label>Senha</label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} style={{ width: "100%", padding: 8 }} /> </div> {error ? <p style={{ color: "crimson" }}>{error}</p> : null} <button type="submit" style={{ padding: "8px 12px" }}>Entrar</button> </form> </div> ); }Detalhes importantes para o fluxo:
- location.state?.from: se o usuário tentou acessar uma rota protegida, o ProtectedRoute enviou para o login com essa informação.
- navigate(from, { replace: true }): evita que o usuário volte ao login ao apertar “voltar” no navegador após autenticar.
9.2 DashboardPage
O Dashboard deve ser simples e útil: mostrar que você está autenticado e oferecer um ponto para evoluir (ex.: cards, dados, etc.).
// src/app/pages/DashboardPage.jsx import { getSession } from "../../auth/services/authService"; export default function DashboardPage() { const session = getSession(); return ( <section> <h3>Dashboard</h3> <p>Bem-vindo, <strong>{session?.user?.name}</strong>.</p> <ul> <li>Aqui você pode listar métricas, atalhos e dados principais.</li> <li>Em seguida, você pode substituir o mock por chamadas HTTP reais.</li> </ul> </section> ); }9.3 ProfilePage
A página de Perfil é um ótimo lugar para validar leitura de “usuário atual” e simular edição. Mesmo sem backend, você pode montar o formulário e preparar a estrutura para salvar depois.
// src/app/pages/ProfilePage.jsx import { useState } from "react"; import { getSession } from "../../auth/services/authService"; export default function ProfilePage() { const session = getSession(); const [name, setName] = useState(session?.user?.name || ""); const [email] = useState(session?.user?.email || ""); function handleSave(e) { e.preventDefault(); alert("Salvar perfil (demo): " + name); } return ( <section> <h3>Perfil</h3> <p>Dados básicos do usuário autenticado.</p> <form onSubmit={handleSave} style={{ maxWidth: 520 }}> <div style={{ marginBottom: 12 }}> <label>Nome</label> <input value={name} onChange={(e) => setName(e.target.value)} style={{ width: "100%", padding: 8 }} /> </div> <div style={{ marginBottom: 12 }}> <label>Email</label> <input value={email} disabled style={{ width: "100%", padding: 8, background: "#f6f6f6" }} /> </div> <button type="submit" style={{ padding: "8px 12px" }}>Salvar</button> </form> </section> ); }Passo 10: declarar as rotas no AppRouter
Agora você vai conectar tudo: rota pública de login e rotas autenticadas sob um “grupo” protegido. Uma forma bem direta é:
/loginrenderizaLoginPage.- Um bloco protegido (ProtectedRoute) envolve as rotas
/appe/app/profile. - O layout autenticado (AppLayout) envolve as páginas internas.
// src/app/router/AppRouter.jsx import { Routes, Route, Navigate } from "react-router-dom"; import LoginPage from "../../auth/pages/LoginPage.jsx"; import ProtectedRoute from "../../shared/components/ProtectedRoute.jsx"; import AppLayout from "../layout/AppLayout.jsx"; import DashboardPage from "../pages/DashboardPage.jsx"; import ProfilePage from "../pages/ProfilePage.jsx"; export default function AppRouter() { return ( <Routes> <Route path="/" element={<Navigate to="/app" replace />} /> <Route path="/login" element={<LoginPage />} /> <Route element={<ProtectedRoute />}> <Route path="/app" element={<AppLayout />}> <Route index element={<DashboardPage />} /> <Route path="profile" element={<ProfilePage />} /> </Route> </Route> <Route path="*" element={<Navigate to="/app" replace />} /> </Routes> ); }Como ler essa árvore:
- ProtectedRoute não tem
path; ele apenas envolve um conjunto de rotas e decide se renderiza oOutletou redireciona. - /app renderiza AppLayout, que por sua vez renderiza as páginas filhas via
Outlet. - index dentro de
/appsignifica que/apprenderiza o Dashboard. - profile vira
/app/profile.
Passo 11: testar o fluxo completo no navegador
Suba o projeto e valide o comportamento esperado.
npm run devChecklist de testes manuais (rápidos):
- Acessar
/appsem sessão deve redirecionar para/login. - Ao fazer login, você deve ir para
/app(ou para a rota pretendida, se veio de um redirecionamento). - No menu, clicar em “Perfil” deve ir para
/app/profile. - Clicar em “Sair” deve limpar a sessão e voltar para
/login. - Tentar acessar
/app/profilediretamente sem sessão deve redirecionar para/login.
Ajustes úteis para deixar a base pronta para crescer
Evitar dependência direta do localStorage nas páginas
Mesmo usando mock, perceba que DashboardPage e ProfilePage chamam getSession() diretamente. Isso funciona, mas cria acoplamento. Um próximo passo natural é centralizar isso em um hook (ex.: useAuth()) e fornecer o usuário via estado, evitando leituras repetidas e facilitando testes.
Neste mini-projeto, mantenha simples. Mas deixe claro o ponto de evolução: páginas deveriam consumir “estado de autenticação” e não “storage”.
Padronizar caminhos e evitar strings soltas
Quando o app cresce, strings como "/app/profile" espalhadas viram fonte de erro. Uma prática comum é criar um arquivo de rotas:
// src/app/router/paths.js export const paths = { login: "/login", app: "/app", profile: "/app/profile" };Mesmo que você não aplique agora, é um ajuste pequeno que evita bugs de digitação e facilita refatorações.
Adicionar um “Home redirect” mais intencional
Você usou / redirecionando para /app. Em apps reais, você pode preferir:
- Se autenticado: ir para
/app. - Se não autenticado: ir para
/login.
Como o guard já cuida disso, redirecionar / para /app é suficiente para este mini-projeto, mas esse é um ponto fácil de refinar.
Erros comuns e como corrigir rapidamente
“Nothing matched location” ao navegar
Isso acontece quando a rota não existe. Verifique:
- Se você declarou
/app/profilecomo rota filhaprofiledentro de/app(correto) e está navegando para/app/profile. - Se o
AppLayouttem<Outlet />.
Dashboard não aparece em /app
Geralmente é falta da rota index. Confirme:
<Route path="/app" element={<AppLayout />}> <Route index element={<DashboardPage />} /> </Route>Logout não redireciona ou sessão não limpa
Verifique se:
logout()remove a chave correta do storage.- Você está usando
navigate("/login", { replace: true })após limpar.
Login não volta para a rota pretendida
Confirme se o ProtectedRoute está passando o state com from: location e se o LoginPage lê location.state?.from?.pathname. Se você abrir /login diretamente, from deve cair no padrão /app.