Angularのルーティングとは、URLとコンポーネントの対応表(ルート定義)を作り、URLに応じて画面の一部を差し替える仕組みです。 現在の Angular では、app.config.ts の provideRouter() に対応表を渡すだけで有効になり、NgModule を用意する必要はありません。

この記事では、ng new で作ったプロジェクトをもとに、設定・リンク・子ルート・404・リダイレクトまでを1つの最小アプリとして全ファイル掲載します。さらに、パラメータ取得の3方式を同じ条件で動かして比べ、ルート定義の順番で404に吸われる例や「NG04002: Cannot match any routes」を実際に出して直します。

確認環境:Ubuntu 24.04 / Node.js 24.21.0 / Angular CLI 22.2.1・Angular 22.2.1(@angular/router 22.2.1)/ TypeScript 6.0.3。動作は ng build と、ng test(Vitest 5.0.3 + jsdom)で確かめています。

ルーティング設定の早見表

やりたいこと 書く場所 書き方
ルーティングを有効にする app.config.ts provideRouter(routes)
URLと画面を対応させる app.routes.ts { path: 'items', component: ItemList }
画面を差し込む場所を作る テンプレート <router-outlet />(RouterOutlet を imports に入れる)
リンクを張る テンプレート <a routerLink="/items">(RouterLink を imports に入れる)
今いるリンクに class を付ける テンプレート routerLinkActive="active"
パラメータ付きのURLにする app.routes.ts { path: 'items/:id', component: ItemDetail }
パラメータを input で受け取る app.config.ts + コンポーネント withComponentInputBinding() + id = input.required<string>()
画面の中にさらに画面を入れる app.routes.ts children: [...] + 親のテンプレートに <router-outlet />
404ページを出す app.routes.ts の最後 { path: '**', component: NotFound }
別のURLへ転送する app.routes.ts { path: 'catalog', redirectTo: 'items' }
タブのタイトルを変える app.routes.ts title: '備品一覧'
コードから移動する コンポーネント inject(Router).navigate(['/items', 3])

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angularルーティング設定|provideRouter・パラメータ・404」

最小アプリの全ファイル(コピペで動く)

ng new demo で作ったプロジェクトの src/app を、次の構成にします。備品の一覧・詳細・登録の画面を持つ小さなアプリです。

src/app/
├── app.config.ts      … provideRouter を書く
├── app.routes.ts      … URLとコンポーネントの対応表
├── app.ts             … ナビゲーションと router-outlet
└── pages/
    ├── home.ts
    ├── item-list.ts
    ├── item-new.ts
    ├── item-detail.ts   … 子ルートを持つ
    ├── item-info.ts     … 子ルート(基本情報)
    ├── item-history.ts  … 子ルート(履歴)
    └── not-found.ts     … 404

src/main.ts は ng new が作ったまま(bootstrapApplication(App, appConfig))で変更しません。app.html と app.css は使わないので削除します。

app.config.ts:provideRouter を登録する

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()),
  ],
};

ng new 直後は provideRouter(routes) だけが書かれています。ここに withComponentInputBinding() を足すと、URLのパラメータをコンポーネントの input で受け取れるようになります。

app.routes.ts:ルート定義

import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { ItemList } from './pages/item-list';
import { ItemNew } from './pages/item-new';
import { ItemDetail } from './pages/item-detail';
import { ItemInfo } from './pages/item-info';
import { ItemHistory } from './pages/item-history';
import { NotFound } from './pages/not-found';

export const routes: Routes = [
  { path: '', component: Home, title: 'ホーム' },
  { path: 'items', component: ItemList, title: '備品一覧' },
  { path: 'items/new', component: ItemNew, title: '備品の登録' },
  {
    path: 'items/:id',
    component: ItemDetail,
    title: '備品の詳細',
    children: [
      { path: '', component: ItemInfo },
      { path: 'history', component: ItemHistory },
    ],
  },
  { path: 'catalog', redirectTo: 'items' },
  { path: '**', component: NotFound, title: 'ページが見つかりません' },
];

app.ts:ナビゲーションと router-outlet

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">ホーム</a>
      <a routerLink="/items" routerLinkActive="active">備品一覧</a>
      <a routerLink="/items/new" routerLinkActive="active">登録</a>
    </nav>
    <main>
      <router-outlet />
    </main>
  `,
  styles: `
    nav a { margin-right: 12px; }
    .active { font-weight: bold; }
  `,
})
export class App {}

pages/home.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  template: `<h1>ホーム</h1>`,
})
export class Home {}

pages/item-list.ts:一覧とクエリパラメータ

import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-item-list',
  imports: [RouterLink],
  template: `
    <h1>備品一覧</h1>
    <p class="cond">並び順: {{ sort() ?? '指定なし' }} / ページ: {{ page() ?? 1 }}</p>
    <ul>
      @for (item of items; track item.id) {
        <li><a [routerLink]="['/items', item.id]">{{ item.name }}</a></li>
      }
    </ul>
    <a routerLink="/items" [queryParams]="{ sort: 'name', page: 2 }" fragment="list">名前順の2ページ目</a>
  `,
})
export class ItemList {
  // クエリパラメータ(?sort=name&page=2)も input で受け取れる
  sort = input<string>();
  page = input<string>();

  items = [
    { id: 1, name: 'ノート' },
    { id: 2, name: 'ペン' },
    { id: 3, name: '付箋' },
  ];
}

pages/item-new.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-item-new',
  template: `<h1>備品の登録</h1>`,
})
export class ItemNew {}

pages/item-detail.ts:パラメータと子ルート

import { Component, input } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-item-detail',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <h1>備品 #{{ id() }}</h1>
    <nav class="tabs">
      <a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">基本情報</a>
      <a routerLink="history" routerLinkActive="active">履歴</a>
      <a [routerLink]="['/items', next()]">次の備品</a>
    </nav>
    <router-outlet />
  `,
})
export class ItemDetail {
  // withComponentInputBinding() により、:id が input に入る
  id = input.required<string>();

  next() {
    return Number(this.id()) + 1;
  }
}

pages/item-info.ts と pages/item-history.ts:子ルートの中身

import { Component } from '@angular/core';

@Component({
  selector: 'app-item-info',
  template: `<p class="tab">基本情報タブ</p>`,
})
export class ItemInfo {}
import { Component } from '@angular/core';

@Component({
  selector: 'app-item-history',
  template: `<p class="tab">履歴タブ</p>`,
})
export class ItemHistory {}

pages/not-found.ts:404ページ

import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-not-found',
  imports: [RouterLink],
  template: `
    <h1>404 ページが見つかりません</h1>
    <a routerLink="/">ホームへ戻る</a>
  `,
})
export class NotFound {}

動かした結果

npx ng build が通ることを確かめたうえで、リンクのクリックとURLの直接指定を順に行うテストを書き、npx ng test --watch=false --reporters=verbose で実行しました。各行は「操作 → 移動後のURL・タブのタイトル・表示された見出し・active が付いたリンク」です。

[/] url=/  title=ホーム
     表示: ホーム  active=["ホーム"]
  クリック「備品一覧」 href=/items
   → url=/items  title=備品一覧
     表示: 備品一覧 | 並び順: 指定なし / ページ: 1  active=["備品一覧"]
  クリック「名前順の2ページ目」 href=/items?sort=name&page=2#list
   → url=/items?sort=name&page=2#list  title=備品一覧
     表示: 備品一覧 | 並び順: name / ページ: 2  active=["備品一覧"]
  クリック「ペン」 href=/items/2
   → url=/items/2  title=備品の詳細
     表示: 備品 #2 | 基本情報タブ  active=["備品一覧","基本情報"]
  クリック「履歴」 href=/items/2/history
   → url=/items/2/history  title=備品の詳細
     表示: 備品 #2 | 履歴タブ  active=["備品一覧","履歴"]
  クリック「次の備品」 href=/items/3
   → url=/items/3  title=備品の詳細
     表示: 備品 #3 | 基本情報タブ  active=["備品一覧","基本情報"]
[/items/new] url=/items/new  title=備品の登録
     表示: 備品の登録  active=["備品一覧","登録"]
[/catalog] url=/items  title=備品一覧
     表示: 備品一覧 | 並び順: 指定なし / ページ: 1  active=["備品一覧"]
[/no/such/page] url=/no/such/page  title=ページが見つかりません
     表示: 404 ページが見つかりません  active=[]

この結果から、次のことが確かめられます。

確かめたこと 結果
routerLink が href に変換される /items、/items/2、/items?sort=name&page=2#list など
title がタブのタイトルになる 移動のたびに切り替わる
:id が input に入る 「備品 #2」「備品 #3」
クエリパラメータも input に入る 「並び順: name / ページ: 2」
子ルートが親の中に表示される 「備品 #2 | 履歴タブ」
redirectTo で転送される /catalog → /items
どれにも合わないURLは ** が受ける /no/such/page → 404(URLはそのまま)

provideRouter と router-outlet の役割

ルーティングを動かす最小の部品は3つです。

部品 役割 書き忘れたとき
provideRouter(routes) ルーターをアプリに登録し、対応表を渡す 実行時に NG0201(下記)
<router-outlet /> URLに合ったコンポーネントを差し込む場所 画面が切り替わらない
RouterOutlet / RouterLink の imports テンプレートでタグや属性を使えるようにする NG8001 / NG8002(下記)

provideRouter の第2引数以降には、withComponentInputBinding() のような追加機能を並べられます。

provideRouter を書き忘れたときのエラー

provideRouter を登録せずに、完成コードの App(routerLink を使っている)を表示しようとした結果です。

NP) NG0201: No provider found for `ActivatedRoute`. Source: DynamicTestModule. Find more at https://v22.angular.dev/errors/NG0201

ActivatedRoute や Router の提供元が見つからない、という NG0201 が出たら、app.config.ts の providers に provideRouter(routes) があるかを確認します(Source: DynamicTestModule はテスト用の環境で動かしたことを示す表示です)。

imports を忘れたときのエラー

app.ts の imports から RouterOutlet を外してビルドした結果です。

✘ [ERROR] NG8001: 'router-outlet' is not a known element:
1. If 'router-outlet' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. Find more at https://next.angular.dev/errors/NG8001 [plugin angular-compiler]

    src/app/app.ts:14:6:
      14 │       <router-outlet />
         ╵       ~~~~~~~~~~~~~~~~~

RouterLink と RouterLinkActive を外した場合は、角かっこ付きの属性でエラーになりました。

✘ [ERROR] NG8002: Can't bind to 'routerLinkActiveOptions' since it isn't a known property of 'a'. Find more at https://next.angular.dev/errors/NG8002 [plugin angular-compiler]

気をつけたいのは、角かっこの無い routerLink="/items" だけを書いた場合です。RouterLink を imports に入れていなくてもビルドは通り、次のように href の無いリンクが出力されました。

import なし: <a routerlink="/items">備品一覧</a>

「リンクを押しても何も起きない」ときは、まずそのコンポーネントの imports に RouterLink があるかを確認してください。

routerLink と routerLinkActive の書き方

パス・クエリ・フラグメントの早見表

URLの各部分と、リンクでの書き方・受け取り方の対応です。

URLの部分 例 リンクでの書き方 受け取り方
パス /items routerLink="/items" (ルート定義で対応づけ)
パスパラメータ /items/2 [routerLink]="['/items', item.id]" id = input.required<string>() または route.snapshot.paramMap.get('id')
クエリパラメータ ?sort=name&page=2 [queryParams]="{ sort: 'name', page: 2 }" sort = input<string>() または route.snapshot.queryParamMap.get('sort')
フラグメント #list fragment="list" route.snapshot.fragment

ActivatedRoute でクエリとフラグメントを読むコンポーネントを、/items?sort=name&page=2#list で表示した結果です。

@Component({ selector: 'app-query', template: `<p>sort={{ sort }} fragment={{ frag }} 全部={{ all }}</p>` })
class QueryReader {
  private route = inject(ActivatedRoute);
  sort = this.route.snapshot.queryParamMap.get('sort');
  frag = this.route.snapshot.fragment;
  all = JSON.stringify(this.route.snapshot.queryParams);
}
/items?sort=name&page=2#list → url=/items?sort=name&page=2#list 表示: sort=name fragment=list 全部={"sort":"name","page":"2"}

URLから受け取る値はすべて文字列です(page が "2" になっています)。数値として使うときは Number() などで変換します。

相対リンクと絶対リンク

/items/2/history?sort=name を表示中に、items/:id のコンポーネント(詳細画面)の中へ置いたリンクが、どの href になるかを出力しました。

書き方 生成された href 意味
routerLink="/items" /items 先頭が / なら絶対パス
routerLink="history" /items/2/history 今のルートの下(子)へ
routerLink="./" /items/2 今のルート自身
routerLink="../" /items 1つ上へ
routerLink="../5" /items/5 1つ上に戻って別のIDへ
[routerLink]="['/items', 7, 'history']" /items/7/history 配列で区切って組み立てる
[routerLink]="['/items']" [queryParams]="{ sort: 'name' }" fragment="list" /items?sort=name#list クエリとフラグメント付き
routerLink="/items" [queryParams]="{ page: 2 }" queryParamsHandling="merge" /items?sort=name&page=2 今のクエリを残して足す

変数を含むパスは、文字列の連結ではなく配列(['/items', item.id])で書くと、区切りの / や記号の扱いを Angular に任せられます。

routerLinkActive:今いる場所を目立たせる

routerLinkActive="active" を付けたリンクは、そのリンク先が今のURLに含まれているとき、class に active が付きます。先ほどの実行結果では、/items/new を開いたとき active=["備品一覧","登録"] と2つに付いていました。/items/new は /items で始まるURLでもあるためです。

指定 判定 向いている使い方
routerLinkActive="active" リンク先が今のURLの先頭部分と一致すれば付く 「備品」配下にいる間ずっと目立たせるメニュー
[routerLinkActiveOptions]="{ exact: true }" を追加 完全に一致したときだけ付く 「ホーム(/)」や、タブの「基本情報(./)」

ホームのリンク(/)は、すべてのURLの先頭に一致してしまうので、exact: true を付けています。

コードから移動する:Router.navigate

ボタンを押したあとの処理などで移動したいときは、Router を注入して navigate を呼びます。実際に呼んだあとの router.url を並べます。

// コンポーネントでは private router = inject(Router); で受け取る(下はテストでの呼び出し)
const router = TestBed.inject(Router);

await router.navigate(['/items', 3], { queryParams: { tab: 'info' }, fragment: 'top' });
await router.navigate(['/items'], { queryParams: { sort: 'name', q: 'ノート 青' } });
await router.navigate([], { queryParams: { page: 3 }, queryParamsHandling: 'merge' });
navigate(['/items', 3], { queryParams, fragment }) → /items/3?tab=info#top
navigate(['/items'], { queryParams }) → /items?sort=name&q=%E3%83%8E%E3%83%BC%E3%83%88%20%E9%9D%92
navigate([], { queryParamsHandling: merge }) → /items?sort=name&q=%E3%83%8E%E3%83%BC%E3%83%88%20%E9%9D%92&page=3

日本語や空白は自動でエンコードされます。3行目のように配列を空にして queryParamsHandling: 'merge' を指定すると、同じ画面のままクエリだけを足せます(ページ送りや絞り込みに便利です)。

パラメータ取得の3方式を比べる

同じ条件で動かす

items/:id に対して、次の3つのコンポーネントを用意し、/items/1 → /items/2 の順に移動して表示を比べます。

// 方式1: snapshot(その時点の値を1回だけ読む)
@Component({ selector: 'app-by-snapshot', template: `<p>snapshot: {{ id }}</p>` })
class BySnapshot {
  private route = inject(ActivatedRoute);
  id = this.route.snapshot.paramMap.get('id');
}

// 方式2: paramMap を購読(変わるたびに受け取る)
@Component({ selector: 'app-by-param-map', template: `<p>paramMap: {{ id() }}</p>` })
class ByParamMap implements OnInit {
  private route = inject(ActivatedRoute);
  id = signal<string | null>(null);
  ngOnInit() {
    this.route.paramMap.subscribe((params) => this.id.set(params.get('id')));
  }
}

// 方式3: withComponentInputBinding + input
@Component({ selector: 'app-by-input', template: `<p>input: {{ id() }}</p>` })
class ByInput {
  id = input.required<string>();
}
R1 snapshot
  /items/1 → url=/items/1 表示: snapshot: 1
  /items/2 → url=/items/2 表示: snapshot: 1
R1 paramMap
  /items/1 → url=/items/1 表示: paramMap: 1
  /items/2 → url=/items/2 表示: paramMap: 2
R1 input
  /items/1 → url=/items/1 表示: input: 1
  /items/2 → url=/items/2 表示: input: 2

snapshot だけ、URLが /items/2 になっても表示が「1」のままです。同じルートの中でパラメータだけが変わる場合、Angular はコンポーネントを作り直さずに使い回します。snapshot は作られた時点の値を1回読むだけなので、変化を拾えません。公式ドキュメント(Read route state)にも、スナップショットは静的で、その後の変化を反映しないと書かれています。

比較表

方式 書く量 同じ画面でIDが変わったとき 必要な設定 向いている場面
route.snapshot.paramMap.get('id') 少ない 追従しない(上の結果) なし 別の画面からしか来ない、開いた時点の値だけでよい
route.paramMap.subscribe(...) 多い 追従する なし 変化のたびに処理を組み立てたい、RxJS の演算子と組み合わせたい
id = input.required<string>() 最も少ない 追従する withComponentInputBinding() 新しく書くコード全般。computed() と相性がよい

完成コードの「次の備品」リンク(/items/2 → /items/3)で見出しが「備品 #3」に変わったのは、方式3を使っているためです。

withComponentInputBinding を付け忘れたとき

provideRouter(routes) だけの状態で、input.required を使うコンポーネントを表示すると、次のエラーになりました。

/items/1 → エラー: NG0950: Input "id" is required but no value is available yet. Find more at https://v22.angular.dev/errors/NG0950

ルーターが input に値を入れてくれないので、必須入力が空のまま読まれた、という状態です。app.config.ts の provideRouter に withComponentInputBinding() を足すと解消します。

公式ドキュメントでは、この機能でパスパラメータのほか、クエリパラメータ、ルートの data、resolve の結果もコンポーネントの入力に結び付く、と説明されています。完成コードの一覧画面で sort と page を input で受け取れているのは、このためです。input の名前は、ルート定義の :id やクエリのキーと同じ名前にします。

子ルート:画面の中で一部だけ切り替える

子ルートは、親のコンポーネントを表示したまま、その中の一部だけを切り替える仕組みです。必要なのは2つだけです。

  1. ルート定義に children を書く
  2. 親コンポーネントのテンプレートに、子を差し込む <router-outlet /> を置く
{
  path: 'items/:id',
  component: ItemDetail,
  children: [
    { path: '', component: ItemInfo },          // /items/2
    { path: 'history', component: ItemHistory }, // /items/2/history
  ],
},
URL 外側の outlet(app.ts) 内側の outlet(item-detail.ts)
/items/2 ItemDetail ItemInfo(path: '' の子)
/items/2/history ItemDetail ItemHistory

子の path には親の部分(items/:id)を書かず、続きの部分だけを書きます。path: '' の子は「親のURLそのままのとき」に表示される既定の子です。

実行結果では、/items/2 → /items/2/history と移動したとき、見出しの「備品 #2」はそのままで、下の部分だけが「基本情報タブ」から「履歴タブ」に変わりました。

ワイルドカードの404と、ルート定義の順番

順番で結果が変わる例

公式ドキュメント(Define routes)には、Angular は最初に一致したルートを採用し、それ以降は調べない(first-match wins)と書かれています。順番を入れ替えて確かめました。

// R2-a::id を先に書いた場合
[
  { path: 'items/:id', component: ByInput },
  { path: 'items/new', component: ItemNew },
]
// R2-b:new を先に書いた場合
[
  { path: 'items/new', component: ItemNew },
  { path: 'items/:id', component: ByInput },
]
R2-a :id を先に書いた場合
  /items/new → url=/items/new 表示: input: new
R2-b new を先に書いた場合
  /items/new → url=/items/new 表示: 備品の登録

:id は「どんな文字でも受け取る」ので、先に書くと new もIDとして受け取ってしまい、登録画面が表示されません。

ワイルドカード ** も同じです。

R2-c ** を先頭に書いた場合
  /items → url=/items 表示: 404 ページが見つかりませんホームへ戻る
R2-d ** を最後に書いた場合
  /items → url=/items 表示: 備品一覧並び順: 指定なし / ページ: 1ノートペン付箋名前順の2ページ目

** は「どのURLにも一致する」ので、先頭に置くとすべてのURLが404に吸われます。並べ方の決まりは「具体的なものを上、あいまいなものを下、ワイルドカードは最後」です。

順番 書くもの 例
1 固定のパス items、items/new
2 パラメータを含むパス items/:id
3 リダイレクト { path: 'catalog', redirectTo: 'items' }
4(最後) ワイルドカード { path: '**', component: NotFound }

NG04002:Cannot match any routes を出して直す

** を書いていない設定で、対応表に無いURLへ移動すると、NG04002 になります。

provideRouter([{ path: 'items', component: ItemList }])
R3 ** が無い設定
  /item → エラー: NG04002: Cannot match any routes. URL Segment: 'item'
  /items/1/history → エラー: NG04002: Cannot match any routes. URL Segment: 'items/1/history'

URL Segment: の後ろに、一致するルートが見つからなかったURLが表示されます。原因と直し方は次のとおりです。

原因 確かめ方 直し方
リンクやURLのつづり違い(item と items) URL Segment: の文字と path を見比べる リンク側か path をそろえる
子ルートを定義していない(items/1/history) children にその path があるか children に追加する
相対リンクの起点が想定と違う 生成された href を確認する 先頭に / を付けて絶対パスにする
利用者が存在しないURLを直接開いた ― 最後に { path: '**', component: NotFound } を置く

** のルートを最後に置いておけば、対応表に無いURLは404ページに案内され、NG04002 は出なくなります(最初の実行結果の /no/such/page)。

redirectTo と、設定時に出るエラー(NG04014)

redirectTo の書き方

{ path: 'catalog', redirectTo: 'items' },             // /catalog → /items
{ path: '', redirectTo: 'items', pathMatch: 'full' }, // トップ → /items

空のパス('')からリダイレクトするときは、pathMatch: 'full' が必要です。付けなかった場合と付けた場合の結果です。

R4-a 空パスの redirectTo に pathMatch なし
  エラー: NG04014: Invalid configuration of route '{path: "", redirectTo: "items"}': please provide 'pathMatch'. The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.
R4-b pathMatch: full を付けた場合
  / → url=/items 表示: 備品一覧並び順: 指定なし / ページ: 1ノートペン付箋名前順の2ページ目

空のパスは、既定の prefix(前方一致)だとすべてのURLの先頭に一致してしまいます。「URL全体が空のときだけ」という意味にするのが pathMatch: 'full' です。

ルート定義の書き方で出る NG04014

ルート定義そのものに不備があると、ルーターが作られる時点で NG04014 が出ます。

R4-c path を / で始めた場合
  エラー: NG04014: Invalid configuration of route '/items': path cannot start with a slash
R4-d component も redirectTo も無い場合
  エラー: NG04014: Invalid configuration of route 'items'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren
メッセージの要点 直し方
please provide 'pathMatch' 空パスのリダイレクトに pathMatch: 'full' を足す
path cannot start with a slash path: '/items' → path: 'items'(ルート定義の path は / で始めない)
One of the following must be provided component・redirectTo・children などのどれかを書く

リンク(routerLink="/items")は / で始め、ルート定義(path: 'items')は / を付けない、という違いを覚えておくと迷いません。

エラーと直し方のまとめ

症状・コード 原因 直し方
NG8001: 'router-outlet' is not a known element RouterOutlet を imports に入れていない コンポーネントの imports に追加
NG8002: Can't bind to 'routerLink...' RouterLink / RouterLinkActive を imports に入れていない 同上
リンクを押しても移動しない(href が無い) 角かっこ無しの routerLink で imports 忘れ RouterLink を imports に追加
NG0201: No provider found for ActivatedRoute provideRouter を登録していない app.config.ts の providers に追加
NG04002: Cannot match any routes 対応表に無いURLへ移動した path を確認、最後に ** を置く
どのURLでも404になる ** が上のほうにある ** を最後へ移す
/items/new が詳細画面になる items/:id が items/new より上にある 固定のパスを上にする
NG04014: please provide 'pathMatch' 空パスの redirectTo pathMatch: 'full'
NG04014: path cannot start with a slash path: '/items' 先頭の / を取る
NG0950: Input "id" is required… withComponentInputBinding() が無い provideRouter に足す
同じ画面でIDを変えても表示が変わらない snapshot で読んでいる input か paramMap の購読にする

チェックリスト

  • [ ] app.config.ts に provideRouter(routes) がある(input で受けるなら withComponentInputBinding() も)
  • [ ] 画面を差し込む場所に <router-outlet /> があり、RouterOutlet を imports に入れた
  • [ ] リンクを書いたコンポーネントの imports に RouterLink(必要なら RouterLinkActive)がある
  • [ ] ルート定義の path は / で始めていない
  • [ ] 固定のパス → パラメータ付き → ** の順に並べた
  • [ ] 空パスの redirectTo に pathMatch: 'full' を付けた
  • [ ] 子ルートを持つコンポーネントのテンプレートに、もう1つ <router-outlet /> を置いた
  • [ ] ホームのリンクなどに [routerLinkActiveOptions]="{ exact: true }" を付けた
  • [ ] 各ルートに title を付けた

よくある質問

Q. RouterModule.forRoot は使わなくてよいのですか? A. ng new で作ったプロジェクトは、app.config.ts の provideRouter(routes) でルーターを登録します。この記事のアプリも NgModule を1つも書かずに動いています。リンクや outlet は、使うコンポーネントの imports に RouterLink や RouterOutlet を入れます。

Q. パラメータは snapshot・paramMap・input のどれで受け取ればよいですか? A. 新しく書くなら withComponentInputBinding() と input の組み合わせが最も短く、同じ画面でIDが変わっても追従します。RxJS の演算子で処理を組み立てたいときは paramMap の購読、開いた時点の値だけで足りるときは snapshot、と使い分けます。

Q. 404ページを表示したとき、URLは変わりますか? A. 変わりません。今回の結果でも、/no/such/page を開くとURLはそのままで、** に指定した NotFound コンポーネントが表示されました。利用者が入力したURLが残るので、どのURLが見つからなかったのかを画面上でも確認できます。

Q. クエリパラメータを残したまま移動するにはどうしますか? A. リンクなら queryParamsHandling="merge"、コードなら navigate のオプションに queryParamsHandling: 'merge' を指定します。今のクエリに新しい値を足した URL(例:/items?sort=name&page=2)になります。

まとめ

  • provideRouter(routes)・<router-outlet />・routerLink の3つで、ルーティングの土台ができる
  • ルート定義は上から順に調べられ、最初に一致したものが使われる。固定のパスを上、:id を下、** は最後に置く
  • パラメータは withComponentInputBinding() + input で受け取ると、短く書けて、同じ画面でのID変更にも追従する。snapshot は開いた時点の値のまま
  • 子ルートは children と、親テンプレートの <router-outlet /> の組み合わせ
  • NG04002 は「対応表に無いURL」。URL Segment: を読み、path を直すか ** の404で受ける
  • 空パスの redirectTo には pathMatch: 'full'、ルート定義の path は / で始めない
  • routerLinkActive は前方一致。完全一致にしたいリンクには exact: true を付ける

参考・出典

  • Angular 公式ドキュメント「Define routes」 https://angular.dev/guide/routing/define-routes
  • Angular 公式ドキュメント「Read route state」 https://angular.dev/guide/routing/read-route-state
  • Angular 日本語ドキュメント「その他のルーティングタスク」 https://angular.jp/guide/routing/common-router-tasks

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angularルーティング設定|provideRouter・パラメータ・404」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?