Typesafe Routes in Angular
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
Typos Lead to Runtime Errors
'/pharmacy/prodcuts'vs'/pharmacy/products'- silently breaks navigation- No IDE autocomplete, no compiler warnings
Refactoring Nightmares
- Changing
/pharmacyto/medicationsrequires finding every string reference - High risk of missing occurrences in templates, services, and guards
- Changing
Missing or Incorrect Parameters
- Forgot to pass
productId? Runtime 404 - Passed parameters in wrong order? Silent failure
- Forgot to pass
No Relationship Between Route Definition and Usage
- Route config says
:productIdbut code uses:id - Impossible to verify without running the app
- Route config says
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/utilsfor 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.pharmacydoesn't exist, TypeScript errors - Refactor
pharmacytomedicationsin 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
- Type safety prevents bugs - Catch routing errors at compile time, not runtime
- Refactoring becomes safe - Change routes with confidence
- Better developer experience - IDE autocomplete and IntelliSense
- Self-documenting code - Routes hierarchy is clear and explicit
- 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
- typesafe-routes Documentation: https://github.com/kruschid/typesafe-routes
- Medikea GitHub: https://github.com/medikea
This article reflects our real-world experience implementing type-safe routing in a production healthcare application serving thousands of users.