Typesafe Routes in Angular

Nov 03, 2025
10 min read
Angular Routing Typescript

Learn how to create typesafe routes in Angular. In this blog post, we will explore the benefits of using typesafe routes and how to implement them in your Angular application.

Type-Safe Routing in Angular: A Game-Changer for Enterprise Applications

Introduction

In modern Angular applications, routing is the backbone of navigation and user experience. However, traditional string-based routing has long been a source of runtime errors, refactoring nightmares, and maintenance headaches. At Medikea, we've adopted typesafe-routes across our healthcare portal applications, and the results have been transformative.

This article explores how typesafe-routes brings compile-time type safety to Angular routing, the bugs it prevents, and the real-world implementation patterns we use in our production codebase.


The Problem with Traditional String-Based Routing

Common Pitfalls

In traditional Angular applications, routes are defined as plain strings:

// Traditional approach - error-prone
export const routes: Routes = [
  { path: "pharmacy/products", component: ProductsComponent },
  { path: "pharmacy/products/new", component: NewProductComponent },
  { path: "pharmacy/products/:productId/edit", component: EditProductComponent },
];

// Navigation - no compile-time safety
this.router.navigate(["/pharmacy/products", productId, "edit"]); // Typo? Wrong parameter? Who knows!

Real-World Bugs This Causes

  1. Typos Lead to Runtime Errors

    • '/pharmacy/prodcuts' vs '/pharmacy/products' - silently breaks navigation
    • No IDE autocomplete, no compiler warnings
  2. Refactoring Nightmares

    • Changing /pharmacy to /medications requires finding every string reference
    • High risk of missing occurrences in templates, services, and guards
  3. Missing or Incorrect Parameters

    • Forgot to pass productId? Runtime 404
    • Passed parameters in wrong order? Silent failure
  4. No Relationship Between Route Definition and Usage

    • Route config says :productId but code uses :id
    • Impossible to verify without running the app
  5. Documentation Drift

    • Routes change but documentation doesn't update
    • New team members guess at available routes

Enter typesafe-routes: Compile-Time Safety for Angular Routing

What is typesafe-routes?

typesafe-routes is a lightweight library that brings TypeScript's type system to routing. It provides:

  • Compile-time type checking for all routes
  • IDE autocomplete for route paths and parameters
  • Refactoring safety - rename routes with confidence
  • Single source of truth for all routing configuration
  • Zero runtime overhead - just TypeScript magic

Implementation at Medikea

1. Centralized Route Definition

We define all routes in a single, shared library that's accessible across our Nx monorepo:

import { createRoutes, str } from "typesafe-routes";

export const routesMap = createRoutes({
  pharmacy: {
    path: ["pharmacy"],
    children: {
      dashboard: { path: ["dashboard"] },
      products: {
        path: ["products"],
        children: {
          new: { path: ["new"] },
          edit: { path: [str("productId"), "edit"] },
        },
      },
    },
  },

  profile: { path: ["profile"] },
});

Key Benefits:

  • Single source of truth - all routes defined in one place
  • Hierarchical structure - mirrors the actual application structure
  • Type-safe parameters - str("productId") defines a required string parameter
  • Shared across modules - exported from @medikea/utils for use anywhere

2. Type-Safe Route Configuration

Using the template() helper from typesafe-routes/angular-router, we convert our type-safe routes to Angular route configs:

import { routesMap } from "@medikea/utils";
import { template } from "typesafe-routes/angular-router";

export const AppRoutes: Routes = [
  {
    path: template(routesMap.pharmacy),
    loadChildren: () => import("./pharmacy/pharmacy.routes").then((r) => r.routes),
  },
  {
    title: "Profile",
    path: template(routesMap.profile),
    loadComponent: () => import("./pages/profile/profile.component"),
  },
];

What Just Happened?

  • template(routesMap.pharmacy) generates the string "pharmacy"
  • Type-checked at compile time - if routesMap.pharmacy doesn't exist, TypeScript errors
  • Refactor pharmacy to medications in one place, and it updates everywhere

3. Nested Routes with the _ Accessor

For child routes, we use the special _ property to access relative paths:

import { routesMap } from "@medikea/utils";
import { template } from "typesafe-routes/angular-router";

export const routes: Routes = [
  {
    title: "Dashboard",
    path: template(routesMap.pharmacy._.dashboard),
    loadComponent: () => import("@medikea/feat-pharmacist-dashboard").then((r) => r.PharmacyDashboardComponent),
  },
  {
    title: "Pharmacy Inventory",
    path: template(routesMap.pharmacy._.products),
    loadComponent: () => import("@medikea/feat-pharmacy-inventory").then((r) => r.PharmacyInventoryComponent),
    children: [
      {
        path: template(routesMap.pharmacy._.products._.new),
        title: "Add New Product",
        loadComponent: () => import("@medikea/feat-pharmacy-inventory").then((r) => r.DrugFormComponent),
      },
      {
        path: template(routesMap.pharmacy._.products._.edit),
        title: "Edit Product",
        loadComponent: () => import("@medikea/feat-pharmacy-inventory").then((r) => r.DrugFormComponent),
      },
    ],
  },
];

Understanding the _ Accessor:

  • routesMap.pharmacy"/pharmacy"
  • routesMap.pharmacy._.dashboard"dashboard" (relative path)
  • routesMap.pharmacy.dashboard"/pharmacy/dashboard" (absolute path)

This distinction is crucial for nested route configurations!

4. Type-Safe Programmatic Navigation

The real magic happens when navigating programmatically. The renderPath() function ensures you pass the correct parameters:

import { renderPath } from "typesafe-routes/angular-router";
import { routesMap } from "@medikea/utils";

export class PharmacyInventoryComponent {
  constructor(private router: Router) {}

  onEditProduct(product: Drug) {
    // TypeScript enforces that you pass { productId: string }
    const path = renderPath(routesMap.pharmacy.products.edit, {
      productId: product.id,
    });
    this.router.navigateByUrl(path);
    // Result: "/pharmacy/products/abc-123/edit"
  }

  onAddProduct() {
    // No parameters needed for this route
    const path = renderPath(routesMap.pharmacy.products.new, {});
    this.router.navigateByUrl(path);
    // Result: "/pharmacy/products/new"
  }
}

Compile-Time Safety in Action:

// ❌ TypeScript error - missing required parameter
const path = renderPath(routesMap.pharmacy.products.edit, {});
// Error: Property 'productId' is missing

// ❌ TypeScript error - wrong parameter name
const path = renderPath(routesMap.pharmacy.products.edit, { id: "123" });
// Error: Object literal may only specify known properties

// ✅ Correct - all parameters provided and typed
const path = renderPath(routesMap.pharmacy.products.edit, { productId: "123" });

5. Type-Safe Navigation Links

We use the same pattern for navigation menus and links:

import { NavItem } from "@medikea/models";
import { routesMap } from "@medikea/utils";
import { template } from "typesafe-routes/angular-router";

export const PHARMACY_NAVIGATION_LIST: NavItem[] = [
  {
    label: "Dashboard",
    icon: "dashboard",
    route: template(routesMap.pharmacy.dashboard),
  },
  {
    label: "Inventory",
    icon: "inventory",
    route: template(routesMap.pharmacy.products),
  },
];

Bugs Prevented by typesafe-routes

1. ✅ Typo Prevention

Before:

// Typo - will silently fail at runtime
this.router.navigate(["/phamracy/prodcts"]); // Oops!

After:

// TypeScript error - property doesn't exist
const path = renderPath(routesMap.phamracy.prodcts, {});
// ^^^^^^^^^ Cannot find name 'phamracy'

2. ✅ Parameter Enforcement

Before:

// Missing productId - runtime 404
this.router.navigate(["/pharmacy/products", "edit"]);

// Wrong parameter order - silent failure
this.router.navigate(["/pharmacy/products/edit", productId]);

After:

// Compile error - missing required parameter
renderPath(routesMap.pharmacy.products.edit, {});
// Error: Property 'productId' is missing

// Compile error - parameters are part of the path template
renderPath(routesMap.pharmacy.products.edit, { productId: "123" });
// Result: "/pharmacy/products/123/edit" - always correct!

3. ✅ Refactoring Safety

Before:

// Change route from /pharmacy to /medications
// Must find and replace ALL occurrences manually:
// - Route configs (multiple files)
// - Navigation calls (scattered across components)
// - Template links (in HTML files)
// - Navigation menus
// - Guards and redirects
// High risk of missing some!

After:

// Change in ONE place:
export const routesMap = createRoutes({
  medications: {
    // Changed from 'pharmacy'
    path: ["medications"],
    children: {
      /* ... */
    },
  },
});

// TypeScript immediately shows ALL places that need updating
// IDE refactoring tools work perfectly
// Compiler won't let you miss anything

4. ✅ Route-Component Consistency

Before:

// Route definition
{ path: 'products/:productId/edit', component: EditComponent }

// Component expects different parameter name
this.route.params.subscribe(params => {
  const id = params['id']; // Wrong! Should be 'productId'
  // Silent failure - id is undefined
});

After:

// Route definition with explicit parameter
edit: {
  path: [str("productId"), "edit"];
}

// TypeScript enforces correct parameter name
const params = renderPath(routesMap.pharmacy.products.edit, { productId: "123" });
// Can only be 'productId' - anything else is a compile error

5. ✅ Documentation as Code

Before:

// Routes are scattered, no clear overview
// Documentation becomes outdated quickly
// New developers must explore the codebase

After:

// Single source of truth - always up to date
export const routesMap = createRoutes({
  pharmacy: {
    path: ["pharmacy"],
    children: {
      dashboard: { path: ["dashboard"] },
      products: {
        path: ["products"],
        children: {
          new: { path: ["new"] },
          edit: { path: [str("productId"), "edit"] },
        },
      },
    },
  },
  // ... clear hierarchy, self-documenting
});

Real-World Impact at Medikea

Metrics

Since adopting typesafe-routes across our hospital portal applications:

  • 🐛 Zero routing-related bugs in production
  • ⏱️ faster refactoring of route structures
  • 👥 Faster onboarding - new developers understand routes immediately
  • 📝 Self-documenting code - no separate routing documentation needed
  • 🔍 Better IDE support - full autocomplete and IntelliSense

Developer Experience

Before typesafe-routes:

// Developer thinks: "What's the route to edit a product?"
// Must search codebase, check route configs, hope it's correct
this.router.navigate(["/pharmacy/products/", id, "/edit"]); // Guessing!

After typesafe-routes:

// Developer types: renderPath(routesMap.
// IDE autocompletes: pharmacy → products → edit
// TypeScript enforces: { productId: string }
const path = renderPath(routesMap.pharmacy.products.edit, { productId: id });
// Confidence: 100%

Best Practices and Patterns

1. Centralize Route Definitions

DO: Keep all routes in a shared library

// libs/shared/utils/src/lib/typesafe-routes.helper.ts
export const routesMap = createRoutes({
  /* all routes */
});

DON'T: Scatter route definitions across multiple files

2. Use Descriptive Parameter Names

DO: Use specific, meaningful names

edit: {
  path: [str("productId"), "edit"];
}

DON'T: Use generic names

edit: {
  path: [str("id"), "edit"];
} // Which id?

3. Mirror Application Structure

DO: Organize routes hierarchically

pharmacy: {
  path: ["pharmacy"],
  children: {
    products: {
      path: ["products"],
      children: {
        new: { path: ["new"] },
        edit: { path: [str("productId"), "edit"] },
      }
    }
  }
}

4. Use template() for Route Configs, renderPath() for Navigation

DO: Use the right helper for the job

// Route configuration
{
  path: template(routesMap.pharmacy.products);
}

// Programmatic navigation
const path = renderPath(routesMap.pharmacy.products.edit, { productId: "123" });

5. Leverage TypeScript's Type System

DO: Let TypeScript catch errors early

// TypeScript will enforce parameter types
const navigateToEdit = (product: Drug) => {
  const path = renderPath(routesMap.pharmacy.products.edit, {
    productId: product.id, // Type-checked!
  });
};

Migration Guide

Step 1: Install typesafe-routes

npm install typesafe-routes
# or
pnpm add typesafe-routes

Step 2: Define Your Routes

Create a centralized route definition file:

import { createRoutes, str } from "typesafe-routes";

export const routesMap = createRoutes({
  // Define your application routes
});

Step 3: Update Route Configurations

Replace string paths with template() calls:

// Before
{ path: 'pharmacy/products', component: ProductsComponent }

// After
import { template } from "typesafe-routes/angular-router";
{ path: template(routesMap.pharmacy.products), component: ProductsComponent }

Step 4: Update Programmatic Navigation

Replace manual path construction with renderPath():

// Before
this.router.navigate(["/pharmacy/products", productId, "edit"]);

// After
import { renderPath } from "typesafe-routes/angular-router";
const path = renderPath(routesMap.pharmacy.products.edit, { productId });
this.router.navigateByUrl(path);

Step 5: Update Templates

Use the generated paths in templates:

// Component
editPath = renderPath(routesMap.pharmacy.products.edit, { productId: this.product.id });

// Template
<a [routerLink]="editPath">Edit Product</a>

Advanced Patterns

Query Parameters

// Define route with query parameters
const path = renderPath(routesMap.pharmacy.products, {});
this.router.navigate([path], {
  queryParams: { category: "medications", sort: "name" },
});

Multiple Parameters

// Define route with multiple parameters
visit: {
  path: [str("patientId"), "visits", str("visitId")];
}

// Use with type safety
const path = renderPath(routesMap.patients.visit, {
  patientId: "123",
  visitId: "456",
});
// Result: "/patients/123/visits/456"

Conclusion

Adopting typesafe-routes has been one of the best architectural decisions we've made at Medikea. It brings the power of TypeScript's type system to Angular routing, eliminating an entire class of bugs and making our codebase more maintainable.

Key Takeaways

  1. Type safety prevents bugs - Catch routing errors at compile time, not runtime
  2. Refactoring becomes safe - Change routes with confidence
  3. Better developer experience - IDE autocomplete and IntelliSense
  4. Self-documenting code - Routes hierarchy is clear and explicit
  5. Zero runtime overhead - Pure TypeScript, no performance impact

Recommendation

If you're building a medium-to-large Angular application, especially in an enterprise or healthcare context where reliability is critical, typesafe-routes is a must-have. The initial investment in setup pays dividends in reduced bugs, faster development, and improved code quality.


Resources


This article reflects our real-world experience implementing type-safe routing in a production healthcare application serving thousands of users.