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

Arquitetura de pastas e padrões de componentes para roteamento e autenticação

Capítulo 2

Tempo estimado de leitura: 12 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Por que a arquitetura de pastas importa em roteamento e autenticação

Em uma SPA com React Router e autenticação via JWT, a arquitetura de pastas não é apenas uma preferência estética: ela determina como você separa responsabilidades, evita acoplamento entre UI e regras de segurança, e mantém o fluxo de navegação previsível. Uma estrutura bem definida facilita três coisas essenciais: (1) localizar rapidamente onde uma regra de acesso está implementada, (2) evitar duplicação de lógica de autenticação em múltiplas telas, e (3) reduzir o risco de “vazamentos” de rotas protegidas por engano.

O objetivo deste capítulo é propor uma organização prática que funcione bem para aplicações de pequeno a grande porte, com padrões de componentes e módulos que tornam o roteamento e a autenticação consistentes. A ideia central é: rotas e guardas (proteções) ficam próximas do “módulo de navegação”, enquanto a autenticação fica encapsulada em um domínio próprio (auth), exposta ao resto do app por uma API pequena (hooks, provider, serviços).

Princípios de organização para SPAs seguras

1) Separação por responsabilidade (não por tipo de arquivo)

Separar “components”, “pages”, “hooks” em pastas globais pode funcionar no início, mas tende a virar um repositório genérico difícil de navegar. Para roteamento e autenticação, é mais sustentável separar por responsabilidade/domínio: um módulo de auth com tudo que diz respeito a login, token, sessão; um módulo de routing com configuração de rotas e guardas; e módulos de features (ex.: dashboard, billing) com suas páginas e componentes específicos.

2) API interna pequena e estável

O resto do app não deveria “saber” como o token é armazenado, como o refresh acontece ou como o usuário é carregado. Em vez disso, consuma uma API interna: useAuth(), AuthProvider, requireAuth (ou um componente <ProtectedRoute />). Isso reduz mudanças em cascata quando você altera detalhes de implementação.

3) Rotas como produto: legibilidade e previsibilidade

Rotas são uma espécie de “contrato” do app. Para manter clareza, evite espalhar definições de rotas em muitos lugares sem padrão. Uma abordagem comum é centralizar o “mapa” de rotas (paths, loaders, guards) e permitir que cada feature exporte suas rotas como um bloco, que é agregado no roteador principal.

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

4) Componentes de guarda (guards) devem ser simples

Guards não devem fazer UI complexa nem chamadas de rede diretamente. Eles devem decidir: renderiza o conteúdo, redireciona, ou mostra um estado de carregamento mínimo. A lógica pesada (carregar usuário, validar token, renovar sessão) fica no módulo de autenticação.

Estrutura de pastas recomendada

A seguir, uma sugestão de estrutura que equilibra modularidade e simplicidade. Ajuste nomes conforme seu padrão, mas mantenha o espírito: auth isolado, roteamento organizado, features independentes.

src/  app/    App.tsx    providers/      AppProviders.tsx    routing/      router.tsx      routes.ts      paths.ts      guards/        ProtectedRoute.tsx        PublicOnlyRoute.tsx        RoleRoute.tsx      layouts/        AppLayout.tsx        AuthLayout.tsx  auth/    AuthProvider.tsx    useAuth.ts    authService.ts    tokenStorage.ts    types.ts    components/      LoginForm.tsx      LogoutButton.tsx    pages/      LoginPage.tsx      CallbackPage.tsx  features/    dashboard/      pages/        DashboardPage.tsx      components/        StatsCard.tsx      routes.tsx    settings/      pages/        SettingsPage.tsx      routes.tsx  shared/    components/      Spinner.tsx      ErrorState.tsx    http/      apiClient.ts      interceptors.ts    utils/      assert.ts      sleep.ts  main.tsx

O que vai em cada pasta

  • app/: composição do aplicativo (providers globais, roteador, layouts). Aqui você “monta” o app.
  • app/routing/: definição do roteador, agregação de rotas, paths e guards. Evite colocar lógica de autenticação aqui; apenas consuma o que auth/ expõe.
  • auth/: domínio de autenticação (estado de sessão, login/logout, persistência de token, tipos, páginas de login). É o único lugar que conhece detalhes do JWT.
  • features/: módulos de negócio. Cada feature pode exportar suas rotas e páginas sem depender de detalhes internos do auth.
  • shared/: componentes e utilitários reutilizáveis (spinner, cliente HTTP, helpers). Evite colocar regras de auth aqui; shared deve ser genérico.

Padrões de componentes para autenticação

AuthProvider + useAuth: estado de sessão centralizado

O padrão mais comum é um AuthProvider que mantém o estado do usuário e expõe ações (login, logout, refresh). O hook useAuth é a porta de entrada para o resto do app.

// src/auth/types.ts export type AuthUser = {   id: string;   name: string;   email: string;   roles: string[]; }; export type AuthState = {   status: 'checking' | 'authenticated' | 'unauthenticated';   user: AuthUser | null; }; export type AuthContextValue = {   state: AuthState;   login: (email: string, password: string) => Promise<void>;   logout: () => void; };
// src/auth/useAuth.ts import { useContext } from 'react'; import { AuthContext } from './AuthProvider'; export function useAuth() {   const ctx = useContext(AuthContext);   if (!ctx) throw new Error('useAuth must be used within AuthProvider');   return ctx; }

O AuthProvider deve ser responsável por: (1) inicializar a sessão ao carregar o app (ex.: ler token do storage e buscar usuário), (2) atualizar o estado quando login/logout ocorrer, e (3) expor um status para o roteamento saber se pode renderizar rotas protegidas.

// src/auth/AuthProvider.tsx import React, { createContext, useEffect, useMemo, useState } from 'react'; import type { AuthContextValue, AuthState } from './types'; import { authService } from './authService'; export const AuthContext = createContext<AuthContextValue | null>(null); export function AuthProvider({ children }: { children: React.ReactNode }) {   const [state, setState] = useState<AuthState>({ status: 'checking', user: null });   useEffect(() => {     let alive = true;     (async () => {       try {         const user = await authService.restoreSession();         if (!alive) return;         if (user) setState({ status: 'authenticated', user });         else setState({ status: 'unauthenticated', user: null });       } catch {         if (!alive) return;         setState({ status: 'unauthenticated', user: null });       }     })();     return () => { alive = false; };   }, []);   const value = useMemo<AuthContextValue>(() => ({     state,     login: async (email, password) => {       const user = await authService.login(email, password);       setState({ status: 'authenticated', user });     },     logout: () => {       authService.logout();       setState({ status: 'unauthenticated', user: null });     },   }), [state]);   return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }

authService + tokenStorage: detalhes do JWT encapsulados

Um erro comum é espalhar acesso a localStorage e manipulação de token por várias telas. Em vez disso, crie um tokenStorage e um authService. Assim, se você trocar localStorage por cookies httpOnly (ou outra estratégia), o impacto fica localizado.

// src/auth/tokenStorage.ts const KEY = 'access_token'; export const tokenStorage = {   get(): string | null {     return localStorage.getItem(KEY);   },   set(token: string) {     localStorage.setItem(KEY, token);   },   clear() {     localStorage.removeItem(KEY);   }, };
// src/auth/authService.ts import { tokenStorage } from './tokenStorage'; import type { AuthUser } from './types'; import { apiClient } from '../shared/http/apiClient'; export const authService = {   async login(email: string, password: string): Promise<AuthUser> {     const { data } = await apiClient.post('/auth/login', { email, password });     tokenStorage.set(data.accessToken);     return data.user as AuthUser;   },   logout() {     tokenStorage.clear();   },   async restoreSession(): Promise<AuthUser | null> {     const token = tokenStorage.get();     if (!token) return null;     const { data } = await apiClient.get('/me');     return data.user as AuthUser;   }, };

Note que o serviço não precisa “decodificar JWT” para tudo. Em muitos cenários, a fonte da verdade do usuário é o backend (/me). Se você decodificar, faça isso apenas para otimizações e nunca como única verificação de autorização.

Padrões de roteamento: layouts, guards e agregação de rotas

Layouts: separar áreas autenticadas e públicas

Layouts ajudam a manter consistência visual e também a organizar rotas por “área”. Um AppLayout pode conter menu, header e outlet; um AuthLayout pode ser usado para login e páginas públicas.

// src/app/routing/layouts/AppLayout.tsx import { Outlet } from 'react-router-dom'; export function AppLayout() {   return (     <div style={{ display: 'grid', gridTemplateColumns: '240px 1fr' }}>       <aside>Menu</aside>       <main><Outlet /></main>     </div>   ); }
// src/app/routing/layouts/AuthLayout.tsx import { Outlet } from 'react-router-dom'; export function AuthLayout() {   return (     <div style={{ maxWidth: 420, margin: '40px auto' }}>       <Outlet />     </div>   ); }

Guards como componentes de rota

Com React Router, um padrão prático é criar componentes que envolvem um <Outlet /> e decidem se a rota pode renderizar. Isso evita repetir checagens em cada página.

// src/app/routing/guards/ProtectedRoute.tsx import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAuth } from '../../../auth/useAuth'; import { Spinner } from '../../../shared/components/Spinner'; import { paths } from '../paths'; export function ProtectedRoute() {   const { state } = useAuth();   const location = useLocation();   if (state.status === 'checking') return <Spinner />;   if (state.status === 'unauthenticated') {     return <Navigate to={paths.login} replace state={{ from: location }} />;   }   return <Outlet />; }

Para páginas que só fazem sentido quando o usuário está deslogado (ex.: login), crie um guard inverso.

// src/app/routing/guards/PublicOnlyRoute.tsx import { Navigate, Outlet } from 'react-router-dom'; import { useAuth } from '../../../auth/useAuth'; import { paths } from '../paths'; export function PublicOnlyRoute() {   const { state } = useAuth();   if (state.status === 'checking') return null;   if (state.status === 'authenticated') return <Navigate to={paths.home} replace />;   return <Outlet />; }

Guard por papel (role-based) como camada adicional

Quando há autorização por papéis, evite colocar if (!user.roles.includes(...)) em cada página. Centralize em um guard. Ele deve assumir que o usuário já está autenticado (ou ser usado dentro de ProtectedRoute).

// src/app/routing/guards/RoleRoute.tsx import { Navigate, Outlet } from 'react-router-dom'; import { useAuth } from '../../../auth/useAuth'; import { paths } from '../paths'; export function RoleRoute({ allowed }: { allowed: string[] }) {   const { state } = useAuth();   if (state.status !== 'authenticated') return <Navigate to={paths.login} replace />;   const ok = state.user?.roles?.some(r => allowed.includes(r));   if (!ok) return <Navigate to={paths.forbidden} replace />;   return <Outlet />; }

Padronizando paths e evitando “strings mágicas”

Um ponto frágil em roteamento é espalhar "/login", "/dashboard" etc. pelo código. Centralize em um arquivo de paths. Isso reduz bugs em refactors e facilita gerar links.

// src/app/routing/paths.ts export const paths = {   home: '/',   login: '/login',   forbidden: '/403',   dashboard: '/app/dashboard',   settings: '/app/settings', } as const;

Em features, você pode exportar paths internos também, mas mantenha o “público” (o que outras áreas usam) centralizado ou reexportado para evitar duplicação.

Agregação de rotas por feature (padrão escalável)

Para crescer sem virar um arquivo gigante de rotas, cada feature pode exportar um bloco de rotas. O módulo de roteamento agrega tudo. Isso cria um padrão: “se quero mexer no dashboard, vou em features/dashboard/routes.tsx”.

// src/features/dashboard/routes.tsx import type { RouteObject } from 'react-router-dom'; import { DashboardPage } from './pages/DashboardPage'; export const dashboardRoutes: RouteObject[] = [   { path: 'dashboard', element: <DashboardPage /> }, ];
// src/features/settings/routes.tsx import type { RouteObject } from 'react-router-dom'; import { SettingsPage } from './pages/SettingsPage'; export const settingsRoutes: RouteObject[] = [   { path: 'settings', element: <SettingsPage /> }, ];

Agora, o roteador principal monta a árvore, aplicando layouts e guards.

// src/app/routing/routes.ts import type { RouteObject } from 'react-router-dom'; import { AppLayout } from './layouts/AppLayout'; import { AuthLayout } from './layouts/AuthLayout'; import { ProtectedRoute } from './guards/ProtectedRoute'; import { PublicOnlyRoute } from './guards/PublicOnlyRoute'; import { RoleRoute } from './guards/RoleRoute'; import { paths } from './paths'; import { LoginPage } from '../../auth/pages/LoginPage'; import { dashboardRoutes } from '../../features/dashboard/routes'; import { settingsRoutes } from '../../features/settings/routes'; export const routes: RouteObject[] = [   {     element: <PublicOnlyRoute />,     children: [       { element: <AuthLayout />, children: [         { path: paths.login, element: <LoginPage /> },       ]},     ],   },   {     path: '/app',     element: <ProtectedRoute />,     children: [       { element: <AppLayout />, children: [         ...dashboardRoutes,         ...settingsRoutes,         {           element: <RoleRoute allowed={['admin']} />,           children: [             { path: 'admin-area', element: <div>Admin</div> },           ],         },       ]},     ],   },   { path: paths.forbidden, element: <div>Sem permissão</div> },   { path: '*', element: <div>404</div> }, ];

Repare em alguns padrões importantes: (1) rotas públicas ficam sob PublicOnlyRoute, (2) rotas autenticadas ficam sob ProtectedRoute, (3) autorização por papel é uma camada interna, aplicada apenas onde necessário, e (4) features exportam apenas rotas relativas ao seu “prefixo” (/app neste exemplo).

Passo a passo prático: implementando a arquitetura do zero no projeto

Passo 1: criar as pastas base

  • Crie src/app/routing com guards, layouts, paths.ts, routes.ts, router.tsx.
  • Crie src/auth com AuthProvider.tsx, useAuth.ts, authService.ts, tokenStorage.ts, pages, components.
  • Crie src/features e mova páginas de negócio para dentro de features.
  • Crie src/shared/http para o cliente HTTP e src/shared/components para UI genérica.

Passo 2: montar o cliente HTTP e conectar token automaticamente

Para evitar passar token manualmente em cada request, centralize no apiClient. Um padrão simples é configurar headers a partir do tokenStorage.

// src/shared/http/apiClient.ts import axios from 'axios'; import { tokenStorage } from '../../auth/tokenStorage'; export const apiClient = axios.create({   baseURL: 'https://api.seudominio.com', }); apiClient.interceptors.request.use((config) => {   const token = tokenStorage.get();   if (token) config.headers.Authorization = `Bearer ${token}`;   return config; });

Esse padrão mantém a autenticação de rede fora das páginas e fora do roteamento.

Passo 3: implementar AuthProvider e envolver o app

Crie um agregador de providers globais. Isso evita poluir main.tsx e mantém o ponto de entrada limpo.

// src/app/providers/AppProviders.tsx import React from 'react'; import { AuthProvider } from '../../auth/AuthProvider'; export function AppProviders({ children }: { children: React.ReactNode }) {   return <AuthProvider>{children}</AuthProvider>; }

Passo 4: criar o router a partir das rotas

// src/app/routing/router.tsx import { createBrowserRouter } from 'react-router-dom'; import { routes } from './routes'; export const router = createBrowserRouter(routes);
// src/app/App.tsx import { RouterProvider } from 'react-router-dom'; import { router } from './routing/router'; export function App() {   return <RouterProvider router={router} />; }
// src/main.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './app/App'; import { AppProviders } from './app/providers/AppProviders'; ReactDOM.createRoot(document.getElementById('root')!).render(   <React.StrictMode>     <AppProviders>       <App />     </AppProviders>   </React.StrictMode> );

Passo 5: criar páginas e componentes de auth sem acoplar ao roteamento

A página de login deve chamar useAuth().login e, após autenticar, redirecionar para a rota de origem (se existir) ou para um padrão. O guard ProtectedRoute já envia state.from ao redirecionar para login.

// src/auth/pages/LoginPage.tsx import { useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from '../useAuth'; import { paths } from '../../app/routing/paths'; export function LoginPage() {   const { login } = useAuth();   const navigate = useNavigate();   const location = useLocation() as any;   const from = location.state?.from?.pathname || paths.dashboard;   async function onSubmit(e: React.FormEvent) {     e.preventDefault();     const form = e.target as HTMLFormElement;     const email = (form.elements.namedItem('email') as HTMLInputElement).value;     const password = (form.elements.namedItem('password') as HTMLInputElement).value;     await login(email, password);     navigate(from, { replace: true });   }   return (     <form onSubmit={onSubmit}>       <h1>Entrar</h1>       <input name="email" placeholder="email" />       <input name="password" type="password" placeholder="senha" />       <button type="submit">Login</button>     </form>   ); }

Repare que a página não conhece token, storage, interceptors. Ela só chama login e navega.

Padrões de componentes e nomenclatura para manter consistência

Pages vs Components

  • Pages: componentes ligados a rotas (ex.: LoginPage, DashboardPage). Devem orquestrar layout e chamadas de hooks/serviços.
  • Components: peças reutilizáveis dentro de uma feature (ex.: StatsCard) ou globais em shared/components.

Container/Presentational (quando usar)

Se uma página ficar grande, separe em: (1) um container que busca dados e lida com estado, e (2) um componente de apresentação que recebe props. Isso ajuda especialmente em telas protegidas, onde você pode testar a UI sem depender do auth.

// exemplo de separação (esqueleto) // DashboardPage.tsx (container) // DashboardView.tsx (presentational)

Barrel exports com cuidado

Você pode usar index.ts para reexportar itens de um módulo (auth, shared), mas evite criar um “mega index” global. Prefira barrels por domínio, para não gerar dependências circulares e imports confusos.

Erros comuns de arquitetura (e como evitar)

1) Guard fazendo chamada de rede

Se o guard chama /me diretamente, você pode disparar múltiplas requisições em re-renderizações e criar estados inconsistentes. Centralize a restauração de sessão no AuthProvider e deixe o guard apenas reagir ao state.status.

2) Token acessado em todo lugar

Quando páginas acessam localStorage para ler token, você perde controle e aumenta risco de bugs. Use tokenStorage e apiClient com interceptor.

3) Rotas protegidas misturadas com públicas sem padrão

Se algumas rotas protegidas estão sob /app e outras soltas, fica fácil esquecer de aplicar ProtectedRoute. Padronize: “tudo que é autenticado fica sob um prefixo e um guard”.

4) Autorização espalhada em páginas

Checagens de role em páginas viram duplicação e inconsistência. Use RoleRoute (ou um helper) e centralize a regra.

Checklist prático para validar sua arquitetura

  • Existe um único lugar responsável por restaurar sessão ao iniciar o app (AuthProvider)?
  • O token é lido/escrito apenas por tokenStorage (ou camada equivalente)?
  • O cliente HTTP injeta o token automaticamente, sem duplicação?
  • Rotas públicas e privadas estão claramente separadas por guards?
  • Features exportam suas rotas e páginas sem depender de detalhes internos do auth?
  • Paths estão centralizados para evitar strings mágicas?
  • Guards são simples e não contêm lógica pesada?

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

Qual abordagem melhor reduz duplicação de lógica e diminui o risco de expor rotas protegidas por engano em uma SPA com React Router e autenticação JWT?

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

Você errou! Tente novamente.

A arquitetura recomendada centraliza a sessão no AuthProvider e expõe uma API pequena via useAuth. Os guards ficam simples, reagindo ao status (checking, authenticated, unauthenticated) para renderizar ou redirecionar, evitando duplicação e vazamentos de rotas.

Próximo capitúlo

Configuração de rotas com React Router e organização por módulos

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