Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Mini-projeto guiado: estrutura base e roteamento do app (Login, Dashboard, Perfil)

Capítulo 18

Tempo estimado de leitura: 9 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

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-dom

Estrutura 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...
Download App

Baixar o aplicativo

spa-auth-router/  src/   main.jsx   App.jsx  package.json

Passo 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.jsx

O 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 é:

  • /login renderiza LoginPage.
  • Um bloco protegido (ProtectedRoute) envolve as rotas /app e /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 o Outlet ou redireciona.
  • /app renderiza AppLayout, que por sua vez renderiza as páginas filhas via Outlet.
  • index dentro de /app significa que /app renderiza o Dashboard.
  • profile vira /app/profile.

Passo 11: testar o fluxo completo no navegador

Suba o projeto e valide o comportamento esperado.

npm run dev

Checklist de testes manuais (rápidos):

  • Acessar /app sem 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/profile diretamente 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/profile como rota filha profile dentro de /app (correto) e está navegando para /app/profile.
  • Se o AppLayout tem <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 LoginPagelocation.state?.from?.pathname. Se você abrir /login diretamente, from deve cair no padrão /app.

Agora responda o exercício sobre o conteúdo:

Qual é o papel do componente ProtectedRoute na estrutura de rotas do app?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O ProtectedRoute atua como guarda: consulta a sessão e, sem token, redireciona para /login. Se autenticado, libera a navegação renderizando as rotas internas pelo Outlet.

Próximo capitúlo

Mini-projeto guiado: autenticação completa, sessão persistente e navegação segura

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.