Angular: Rotas, Guardas e Navegação

As rotas no Angular formam um esquema de navegação completo, simples e fácil de usar, que inclui guardas para proteger o acesso às páginas da sua aplicação. Se você ainda não sabe como elas funcionam, ou quer se aprofundar no assunto, vem comigo que neste artigo eu mostro tudo, passo a passo.

Ao longo do texto, vamos criar um módulo de rotas, exibir páginas, navegar pelo HTML e pelo TypeScript, ler parâmetros, aninhar rotas e, por fim, aplicar uma guarda. Para isso, usamos a abordagem baseada em módulos (NgModule), com o AppRoutingModule e o RouterModule.

Criando o módulo de rotas no Angular

No Angular, muita coisa é modularizada, e as rotas seguem a mesma ideia. Ao criar uma aplicação com o Angular CLI, a ferramenta pergunta se você quer adicionar o roteamento (Would you like to add Angular routing?).

Ao responder "Yes", você já recebe tudo o que precisa para o roteamento. Na verdade, basta ter um arquivo de módulo de rotas e importá-lo no módulo raiz da aplicação.

Esse arquivo, normalmente chamado app-routing.module.ts, tem a seguinte estrutura:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// Array que vai receber a definição das rotas
const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Neste momento, o que importa é a constante routes. É nela que vamos montar toda a estrutura de rotas da aplicação.

O método RouterModule.forRoot registra as rotas e os serviços do roteador no nível raiz. Além disso, o módulo exporta o RouterModule, o que libera as diretivas de navegação para os componentes do AppModule.

Gerando as páginas de exemplo

Para exemplificar melhor, vamos criar algumas páginas, que no Angular são componentes. Execute os comandos abaixo no terminal, dentro da pasta do projeto:

ng generate component pages/login
ng generate component pages/signup
ng generate component pages/master
ng generate component pages/home
ng generate component pages/reports

Cada comando cria uma pasta com o componente, o template HTML, o arquivo de estilo e o arquivo de testes. Em projetos baseados em módulos, o CLI também declara o componente no AppModule automaticamente.

Compondo as rotas no Angular

Cada rota tem dois elementos principais: o path, que é o caminho da URL, e o component, que é o componente a carregar. Existem ainda formas mais eficientes de carregar páginas, como o carregamento sob demanda por módulos, mas esse é assunto para um próximo artigo.

O código abaixo mostra a forma mais simples de compor rotas no Angular:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './pages/login/login.component';
import { SignupComponent } from './pages/signup/signup.component';
import { MasterComponent } from './pages/master/master.component';
import { HomeComponent } from './pages/home/home.component';
import { ReportsComponent } from './pages/reports/reports.component';

// Cada rota associa um caminho a um componente
const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent },
  { path: 'master', component: MasterComponent },
  { path: 'home', component: HomeComponent },
  { path: 'reports', component: ReportsComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Repare que o path não leva a barra inicial. Assim, a rota home corresponde à URL http://localhost:4200/home.

Ao abrir o navegador nesse endereço, porém, você continua vendo a página padrão do Angular. Isso acontece porque ainda falta dizer onde o componente deve aparecer.

Vale destacar também que a ordem das rotas importa. O roteador usa a estratégia de primeira correspondência, então rotas mais específicas devem vir antes das genéricas, e um curinga como ** fica sempre por último.

Exibindo as páginas com o Router Outlet

Quando você configura uma rota e navega até ela, o Angular lê a URL, procura a rota correspondente e tenta carregar o componente na tela. No nosso caso, ele encontrou a rota home e o HomeComponent, mas não descobriu onde exibir esse componente.

Para resolver isso, precisamos de uma tag especial chamada Router Outlet. Sempre que o fluxo descrito acima acontece, o Angular procura essa tag para renderizar o componente desejado.

Então, abra o arquivo app.component.html, remova todo o conteúdo e deixe apenas a tag abaixo:

<router-outlet></router-outlet>

Dessa forma, ao navegar para http://localhost:4200/home, você já vê o texto "home works!", que é o conteúdo padrão dos componentes gerados pelo CLI.

Note que qualquer HTML presente no app.component.html aparece em todas as páginas da aplicação. Por isso, particularmente, prefiro deixar apenas o Router Outlet nesse arquivo.

Parâmetros nas rotas no Angular

Para passar parâmetros nas rotas no Angular, primeiro defina o nome do parâmetro no path, usando dois-pontos:

// O trecho :param vira um parâmetro da rota
{ path: 'home/:param', component: HomeComponent },

Feito isso, navegue para http://localhost:4200/home/my-param. Em seguida, recupere o valor pelo serviço ActivatedRoute:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  constructor(private activatedRoute: ActivatedRoute) { }

  ngOnInit() {
    // Lê o valor do parâmetro a partir do snapshot da rota
    const param = this.activatedRoute.snapshot.paramMap.get('param');
    console.log(param);
  }
}

O ActivatedRoute dá acesso à rota ativa no momento, aos seus parâmetros e a outros valores, como query params e dados estáticos. Já o snapshot traz uma fotografia da rota no instante em que o componente nasceu.

Se o mesmo componente puder receber parâmetros diferentes sem ser recriado, prefira a propriedade paramMap como Observable. Nesse caso, você assina as mudanças e reage a cada novo valor.

Rotas aninhadas no Angular

Normalmente, as páginas de login, cadastro e recuperação de senha não têm menu lateral nem cabeçalho. Por outro lado, páginas como Home e Reports costumam ter.

Aqui cabe o conceito de Master Pages, em que temos uma página mestre e carregamos outro conteúdo dentro dela. Para chegar a esse resultado com rotas aninhadas no Angular, basta mover uma rota para dentro da outra com a propriedade children.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './pages/login/login.component';
import { SignupComponent } from './pages/signup/signup.component';
import { MasterComponent } from './pages/master/master.component';
import { HomeComponent } from './pages/home/home.component';
import { ReportsComponent } from './pages/reports/reports.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent },
  {
    // A Master envolve as páginas filhas
    path: '',
    component: MasterComponent,
    children: [
      { path: '', component: HomeComponent },
      { path: 'reports', component: ReportsComponent },
    ],
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Agora, Home e Reports ficam dentro de Master na árvore de rotas. Consequentemente, o ciclo de renderização dos componentes segue esse mesmo modelo.

Eu removi o path da Master e da Home porque não fazia mais sentido mantê-lo. Caso contrário, as URLs seriam master/home e master/reports, e nunca teríamos apenas a /.

Um segundo Router Outlet na página mestre

Seguindo o ciclo, ao renderizar a tela o Angular tenta carregar dois componentes, como a Master e a Home. Porém, só existe um Router Outlet na tela, o que gera confusão.

Portanto, a Master também precisa do seu próprio Router Outlet:

<p>master works!</p>
<hr>
<a routerLink="/login" routerLinkActive="selected">Login</a><br>
<a routerLink="/signup" routerLinkActive="selected">Signup</a><br>
<a routerLink="/" routerLinkActive="selected">Home</a><br>
<a routerLink="/reports" routerLinkActive="selected">Reports</a><br>
<hr>
<h3>Menu/Header</h3>
<!-- Aqui o Angular renderiza as páginas filhas -->
<router-outlet></router-outlet>

Ao navegar para Home ou Reports, você vê os links e também o texto "Menu/Header". Ou seja, a Master funciona como moldura, enquanto o conteúdo das filhas muda dentro dela.

Guardas de rotas no Angular

Para fechar o assunto de rotas no Angular, temos as guardas, que permitem ou bloqueiam o acesso a determinadas rotas. Elas são cumulativas, ou seja, você pode aplicar vários arquivos de guarda à mesma rota.

Na abordagem baseada em classes, todas seguem o mesmo padrão. Cada guarda implementa a interface CanActivate e tem um único método, canActivate, que retorna verdadeiro ou falso para indicar se o usuário pode acessar a página.

Além do CanActivate, o roteador oferece outros tipos. O CanActivateChild protege rotas filhas, o CanDeactivate impede a saída de uma página e o CanMatch decide se uma rota deve ou não ser considerada na correspondência.

Criando a guarda

Neste exemplo, criei uma guarda que sempre retorna falso. Ou seja, ela nunca permite a navegação para a página protegida.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class SampleGuard implements CanActivate {
  // Retornar false bloqueia a navegação
  canActivate() {
    return false;
  }
}

Em um cenário real, a guarda consulta um serviço de autenticação. Assim, ela libera a rota apenas para usuários logados ou com o perfil adequado.

O método também aceita outros tipos de retorno. Além de boolean, ele pode devolver um UrlTree para redirecionar o usuário, ou ainda uma Promise ou um Observable, caso a decisão dependa de uma chamada assíncrona.

Registrando a guarda no AppModule

Feito isso, adicione a guarda aos providers do AppModule:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './pages/login/login.component';
import { SignupComponent } from './pages/signup/signup.component';
import { MasterComponent } from './pages/master/master.component';
import { HomeComponent } from './pages/home/home.component';
import { ReportsComponent } from './pages/reports/reports.component';
import { SampleGuard } from './guards/sample.guard';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    SignupComponent,
    MasterComponent,
    HomeComponent,
    ReportsComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [
    // Registra a guarda no container de injeção de dependência
    SampleGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Aplicando a guarda às rotas no Angular

Para finalizar, use a propriedade canActivate da rota para aplicar a guarda a uma rota pai ou a uma rota filha:

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent },
  {
    path: '',
    component: MasterComponent,
    // Todas as rotas filhas herdam essa proteção
    canActivate: [SampleGuard],
    children: [
      { path: '', component: HomeComponent },
      { path: 'reports', component: ReportsComponent },
    ],
  },
];

Agora, ao tentar navegar para / ou para /reports, o roteador bloqueia o acesso. Como a guarda está na rota pai, ela protege todas as filhas de uma só vez.

Se você quiser ver esse conceito aplicado a um fluxo real de autenticação, confira o artigo sobre login, logout, proteção de rotas e envio de tokens com Angular.

Guardas funcionais nas versões recentes

A partir do Angular 15, o framework passou a recomendar guardas funcionais, escritas como funções simples do tipo CanActivateFn. Nessas versões, a documentação marcou a abordagem baseada em classes como obsoleta.

Na guarda funcional, você obtém as dependências com a função inject(), sem precisar de classe nem de registro em providers. Mesmo assim, o raciocínio continua o mesmo: a função retorna verdadeiro, falso ou um redirecionamento.

Se o seu projeto usa uma versão recente, consulte o guia oficial de route guards do Angular para adaptar o exemplo.

Guardas não substituem a segurança no servidor

Uma guarda melhora a experiência do usuário, mas não protege dados por si só. Afinal, todo código que roda no navegador pode ser inspecionado e alterado.

Por isso, a própria documentação do Angular reforça que a autorização precisa acontecer também no servidor. Em outras palavras, a API deve validar o token e as permissões em cada requisição.

Rotas curinga e redirecionamentos no Angular

Até aqui, toda URL digitada precisava existir na lista de rotas. Mas o que acontece quando o usuário acessa um endereço que não existe, como /abc?

Nesse caso, o roteador não encontra correspondência e a navegação falha. Para tratar esse cenário, usamos uma rota curinga, com o path igual a **, que aponta para uma página de erro.

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent },
  // Redireciona a raiz para a página de login
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  // Captura qualquer URL que não corresponda às rotas anteriores
  { path: '**', component: NotFoundComponent },
];

Repare na propriedade redirectTo, que envia o usuário para outra rota. Já o pathMatch: 'full' garante que o redirecionamento ocorra apenas quando a URL inteira estiver vazia, e não em qualquer caminho que comece com vazio.

Como o roteador escolhe a primeira rota que corresponde à URL, a rota curinga deve ficar sempre no fim do array. Caso contrário, ela captura tudo e as demais rotas no Angular nunca chegam a ser avaliadas.

O componente NotFoundComponent é apenas uma página comum, gerada com ng generate component pages/not-found. Nela, você pode exibir uma mensagem amigável e um link de volta para a página inicial.

Formação completa de Angular

E aí, gostou deste artigo? Se você quer aprender a criar um app completo com autenticação, restrição de rotas e perfis de acesso, conheça a formação em Angular do balta.io.

O curso é totalmente prático, com uma didática diferenciada, e você pode começar de graça.

Boas práticas para organizar rotas no Angular

À medida que a aplicação cresce, o arquivo de rotas tende a ficar extenso. Por isso, vale adotar alguns cuidados desde o início.

  • Mantenha as rotas no Angular em um módulo próprio, separado do AppModule.
  • Declare as rotas mais específicas antes das genéricas e deixe o curinga por último.
  • Agrupe páginas que compartilham layout sob uma rota pai com children.
  • Aplique as guardas na rota pai sempre que todas as filhas exigirem a mesma regra.
  • Prefira o routerLink ao href em todos os links internos.

Além disso, use nomes de caminho curtos, em minúsculas e sem acentos. Assim, as URLs ficam legíveis e fáceis de compartilhar.

Conclusão

Neste artigo, você viu como as rotas no Angular organizam a navegação de uma aplicação, desde o módulo de rotas até as guardas. Primeiro, criamos as rotas e o Router Outlet; em seguida, navegamos pelo HTML e pelo TypeScript.

Depois, lemos parâmetros com o ActivatedRoute, montamos rotas aninhadas com uma página mestre e protegemos o acesso com uma guarda. Com essa base, você já consegue estruturar a navegação de praticamente qualquer SPA.

O próximo passo natural é estudar o carregamento sob demanda e integrar as guardas a um serviço de autenticação real. Assim, a aplicação fica mais leve e mais segura ao mesmo tempo.

FAQ

O href recarrega a aplicação inteira a cada clique. Já o routerLink troca apenas os componentes necessários, mantendo o estado da SPA.

Para que serve o router-outlet?

Ele marca o lugar da tela onde o Angular renderiza o componente da rota ativa. Em rotas aninhadas, cada componente pai precisa do seu próprio router-outlet.

Como ler um parâmetro da URL no Angular?

Injete o ActivatedRoute e use snapshot.paramMap.get('nome'). Se o parâmetro mudar sem recriar o componente, assine o Observable paramMap.

Uma guarda pode redirecionar o usuário?

Sim. Em vez de retornar false, a guarda pode retornar um UrlTree apontando para a página de login, por exemplo.

As guardas garantem a segurança da aplicação?

Não sozinhas, porque o código do navegador pode ser alterado. A validação de permissões precisa acontecer também na API.

Qual a diferença entre CanActivate e CanActivateChild?

O CanActivate protege a própria rota em que está configurado. O CanActivateChild roda antes de ativar cada rota filha.

Glossário

Termo Significado
SPA Single Page Application, aplicação que troca o conteúdo da página sem recarregá-la.
Router Serviço do Angular responsável por interpretar URLs e navegar entre componentes.
Router Outlet Tag que indica onde o Angular renderiza o componente da rota ativa.
routerLink Diretiva que cria links de navegação sem recarregar a página.
routerLinkActive Diretiva que aplica classes CSS ao link da rota ativa.
ActivatedRoute Serviço que expõe informações da rota ativa, como parâmetros e dados.
Rota aninhada Rota declarada dentro da propriedade children de outra rota.
Guarda Lógica que permite ou bloqueia a navegação para uma rota.
UrlTree Representação de uma URL que a guarda pode retornar para redirecionar o usuário.
NgModule Decorador que agrupa componentes, diretivas e serviços em um módulo do Angular.

Fontes