Angular Route Parameters and Query Parameters

Angular Tutorial: Beginner to Advanced · 9. Routing and navigation · 4 min read · Updated

Many pages depend on the URL: /products/3 shows product 3, and /products?sort=price sorts a list. In this lesson you build a list page and a detail page and read values from the URL.

What you will learn

  • How to define a route with a parameter such as :id
  • How to read it with component input binding
  • How to use query parameters and navigate from code

A small product service

// src/app/products/product-data.ts
export interface Product {
  id: number;
  name: string;
  price: number;
}

export const PRODUCTS: Product[] = [
  { id: 1, name: 'Notebook', price: 5 },
  { id: 2, name: 'Backpack', price: 40 },
  { id: 3, name: 'Desk lamp', price: 25 },
];

The list and detail pages

// src/app/products/product-list.ts
import { Component, computed, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PRODUCTS } from './product-data';

@Component({
  selector: 'app-product-list',
  imports: [RouterLink],
  template: `
    <h2>Products</h2>
    <p>
      Sort:
      <a routerLink="/products" [queryParams]="{ sort: 'name' }">by name</a> |
      <a routerLink="/products" [queryParams]="{ sort: 'price' }">by price</a>
    </p>
    <ul>
      @for (p of items(); track p.id) {
        <li><a [routerLink]="['/products', p.id]">{{ p.name }}</a> ({{ p.price }} USD)</li>
      }
    </ul>
  `,
})
export class ProductList {
  // Filled from the query string: /products?sort=price
  readonly sort = input<string>();

  protected readonly items = computed(() => {
    const list = [...PRODUCTS];
    return this.sort() === 'price'
      ? list.sort((a, b) => a.price - b.price)
      : list.sort((a, b) => a.name.localeCompare(b.name));
  });
}
// src/app/products/product-detail.ts
import { Component, computed, inject, input } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { PRODUCTS } from './product-data';

@Component({
  selector: 'app-product-detail',
  imports: [RouterLink],
  template: `
    @if (product(); as p) {
      <h2>{{ p.name }}</h2>
      <p>Price: {{ p.price }} USD</p>
      <button type="button" (click)="next()">Next product</button>
    } @else {
      <h2>Product not found</h2>
    }
    <p><a routerLink="/products">Back to the list</a></p>
  `,
})
export class ProductDetail {
  private readonly router = inject(Router);

  // Filled from the path: /products/:id
  readonly id = input.required<string>();

  protected readonly product = computed(() =>
    PRODUCTS.find((p) => p.id === Number(this.id())),
  );

  protected next(): void {
    const current = Number(this.id());
    const nextId = current >= PRODUCTS.length ? 1 : current + 1;
    this.router.navigate(['/products', nextId]);
  }
}

Routes and router setup

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { About } from './pages/about';
import { NotFound } from './pages/not-found';
import { ProductList } from './products/product-list';
import { ProductDetail } from './products/product-detail';

export const routes: Routes = [
  { path: '', component: Home, title: 'Home' },
  { path: 'about', component: About, title: 'About' },
  { path: 'products', component: ProductList, title: 'Products' },
  { path: 'products/:id', component: ProductDetail, title: 'Product details' },
  { path: '**', component: NotFound, title: 'Not found' },
];
// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding()),
  ],
};
<!-- src/app/app.html -->
<header>
  <nav aria-label="Main">
    <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
    <a routerLink="/products" routerLinkActive="active">Products</a>
    <a routerLink="/about" routerLinkActive="active">About</a>
  </nav>
</header>
<main>
  <router-outlet />
</main>

The key parts

  • path: 'products/:id' defines a parameter. The colon marks id as a variable part of the URL.
  • withComponentInputBinding() sends route parameters and query parameters into component inputs with the same name. No ActivatedRoute code is needed for simple cases.
  • Route parameters are always strings, so convert with Number(...) when you need a number.
  • [routerLink]="['/products', p.id]" builds the URL /products/1 from an array of segments.
  • [queryParams]="{ sort: 'price' }" adds ?sort=price. Query parameters are optional, so use input<string>() and handle undefined.
  • router.navigate([...]) changes the page from code.

Try it yourself

  1. Add a category query parameter and filter the list.
  2. Show a friendly message when the id in the URL does not match any product.
  3. Add a "Previous product" button next to "Next product".

Quick check

  1. How do you declare a route parameter?
  2. What type is a route parameter value?
  3. What does withComponentInputBinding() do?

Next, continue with Angular Router Navigation: ActivatedRoute, Query Params, Route Data and Router Events.

Frequently asked questions

When should I use ActivatedRoute instead of input binding?

Input binding covers most cases. Use ActivatedRoute when you need to react to parameters through observables, for example to combine them with other streams.

Why does the page not reload when I go from product 1 to product 2?

Angular reuses the same component and just updates the input. Because we used computed(), the page updates on its own.