DEV Community

Cover image for Stop Hardcoding Routes: The Angular 19 Pattern That Will Save Your Sanity πŸš€
jayasooriya-s
jayasooriya-s

Posted on

Stop Hardcoding Routes: The Angular 19 Pattern That Will Save Your Sanity πŸš€

Ever found yourself doing Ctrl+F across 50+ files just to change one route? Yeah, me too. Here's how I fixed it once and for all.


Picture this: It's 2 AM, you're on your third cup of coffee, and your PM just dropped the bomb – "Hey, can we change all the /products routes to /inventory? Should be quick, right?"

If you've been there, you know the pain. You fire up your IDE, start the great hunt for hardcoded routes, and pray you don't miss any. There's got to be a better way, right?

Spoiler alert: There is. And it's going to change how you think about routing in Angular forever.

The Nightmare We All Know Too Well

Let's be honest – we've all written code like this:

// 😱 The route nightmare that haunts us all
viewProduct(id: string): void {
  this.router.navigate(['/products/details', id]); // Hardcoded disaster
}

editProduct(id: string): void {
  this.router.navigate(['/products/edit', id]); // Another one bites the dust
}

createProduct(): void {
  this.router.navigate(['/products/create']); // The trilogy of terror
}
Enter fullscreen mode Exit fullscreen mode

Now imagine this scattered across 20 components. Then your client decides they want to rebrand "products" to "items".

RIP your weekend plans. πŸ’€

The Game-Changer: Route Constants That Actually Work

After getting burned by this pattern one too many times, I developed a centralized approach that's been a lifesaver. Here's the magic:

// constants/routes.constants.ts
export const ROUTES = {
  PRODUCTS: {
    // For your route config (the boring stuff)
    ROOT: '',
    DETAILS_PARAM: 'details/:id',
    CREATE: 'create',
    EDIT_PARAM: 'edit/:id',

    // Navigation helpers (the good stuff)
    nav: {
      root: () => ['/products'],
      details: (id: string | number) => ['/products', 'details', id.toString()],
      create: () => ['/products', 'create'],
      edit: (id: string | number) => ['/products', 'edit', id.toString()]
    },

    // URL builders (for when you need actual strings)
    url: {
      root: () => '/products',
      details: (id: string | number) => `/products/details/${id}`,
      create: () => '/products/create',
      edit: (id: string | number) => `/products/edit/${id}`
    }
  }
} as const; // ← This 'as const' is crucial for type safety!
Enter fullscreen mode Exit fullscreen mode

Angular 19 Standalone Routes: Clean AF

// products.routes.ts
export const PRODUCT_ROUTES: Routes = [
  {
    path: ROUTES.PRODUCTS.ROOT,
    canActivate: [authGuard],
    children: [
      { path: ROUTES.PRODUCTS.ROOT, component: ProductListComponent },
      { path: ROUTES.PRODUCTS.DETAILS_PARAM, component: ProductDetailsComponent },
      { path: ROUTES.PRODUCTS.CREATE, component: ProductFormComponent },
      { path: ROUTES.PRODUCTS.EDIT_PARAM, component: ProductFormComponent }
    ]
  }
];
Enter fullscreen mode Exit fullscreen mode

Your Components Will Thank You Later

// πŸŽ‰ Look ma, no hardcoded routes!
viewProduct(id: string): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.details(id));
}

editProduct(id: string): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.edit(id));
}

createProduct(): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.create());
}

// For templates and sharing
getProductUrl(id: string): string {
  return ROUTES.PRODUCTS.url.details(id);
}
Enter fullscreen mode Exit fullscreen mode

Different Types of Navigation: Master Them All 🧭

1. Simple Route Navigation

// Basic navigation without parameters
goToProducts(): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.root());
}

// Navigate with single parameter
viewUser(userId: string): void {
  this.router.navigate(ROUTES.USERS.nav.profile(userId));
}
Enter fullscreen mode Exit fullscreen mode

2. Navigation with Query Parameters

// Navigate with query params
searchProducts(category: string, minPrice: number): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.root(), {
    queryParams: { 
      category, 
      minPrice,
      page: 1 
    }
  });
}

// Preserve existing query params
editProduct(id: string): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.edit(id), {
    queryParamsHandling: 'preserve'
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Navigation with Fragments and Extras

// Navigate to section with fragment
viewProductReviews(id: string): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.details(id), {
    fragment: 'reviews'
  });
}

// Navigate and replace current history
replaceCurrentRoute(id: string): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.details(id), {
    replaceUrl: true
  });
}

// Navigate with state data
navigateWithState(id: string, productData: any): void {
  this.router.navigate(ROUTES.PRODUCTS.nav.edit(id), {
    state: { productData, returnUrl: this.router.url }
  });
}
Enter fullscreen mode Exit fullscreen mode

Navigation vs URLs: Know the Difference

Here's something that tripped me up initially:

Use .nav for Router Navigation

// βœ… For programmatic navigation
this.router.navigate(ROUTES.PRODUCTS.nav.details(id));
// Produces: ['/products', 'details', '123']
Enter fullscreen mode Exit fullscreen mode

Use .url for Everything Else

// βœ… For templates, sharing, APIs, etc.
const shareUrl = ROUTES.PRODUCTS.url.details(id);
// Produces: '/products/details/123'
Enter fullscreen mode Exit fullscreen mode

Real-World Magic ✨

Email Notifications That Actually Work

async sendProductAlert(productId: string, userEmail: string): Promise<void> {
  const productUrl = `${window.location.origin}${ROUTES.PRODUCTS.url.details(productId)}`;

  await this.emailService.send({
    to: userEmail,
    subject: 'πŸ”” Product Update Alert',
    html: `<a href="${productUrl}">View Product Details β†’</a>`
  });
}
Enter fullscreen mode Exit fullscreen mode

Social Sharing Made Simple

shareProduct(productId: string): void {
  const url = ROUTES.PRODUCTS.url.details(productId);
  const fullUrl = `${window.location.origin}${url}`;

  if (navigator.share) {
    navigator.share({
      title: "'Check out this amazing product!',"
      url: fullUrl
    });
  } else {
    navigator.clipboard.writeText(fullUrl);
    this.showToast('Link copied to clipboard!');
  }
}
Enter fullscreen mode Exit fullscreen mode

Breadcrumbs Without the Headache

// In your component
homeUrl = '/dashboard';
productsUrl = ROUTES.PRODUCTS.url.root();

// Template usage
// <a [routerLink]="homeUrl">🏠 Home</a>
// <a [routerLink]="productsUrl">πŸ“¦ Products</a>
Enter fullscreen mode Exit fullscreen mode

Testing: No More Route Guessing Games

describe('ProductListComponent', () => {
  let component: ProductListComponent;
  let router: jasmine.SpyObj<Router>;

  beforeEach(async () => {
    const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
    // ... setup code
  });

  it('should navigate to product details like a boss', () => {
    const productId = '123';
    component.viewProduct(productId);

    // βœ… Clear, predictable expectations
    expect(router.navigate).toHaveBeenCalledWith(
      ROUTES.PRODUCTS.nav.details(productId)
    );
  });

  it('should generate correct URLs for sharing', () => {
    const productId = '456';
    const expectedUrl = '/products/details/456';

    const actualUrl = component.getProductUrl(productId);
    expect(actualUrl).toBe(expectedUrl);
  });
});
Enter fullscreen mode Exit fullscreen mode

Scale Like a Pro πŸ“ˆ

As your app grows, adding features becomes a breeze:

export const ROUTES = {
  PRODUCTS: {
    // ... existing product routes
  },

  ORDERS: {
    ROOT: '',
    DETAILS_PARAM: 'details/:id',
    TRACKING_PARAM: 'tracking/:trackingNumber',

    nav: {
      root: () => ['/orders'],
      details: (id: string) => ['/orders', 'details', id],
      tracking: (trackingNumber: string) => ['/orders', 'tracking', trackingNumber]
    },

    url: {
      root: () => '/orders',
      details: (id: string) => `/orders/details/${id}`,
      tracking: (trackingNumber: string) => `/orders/tracking/${trackingNumber}`
    }
  },

  CUSTOMERS: {
    ROOT: '',
    PROFILE_PARAM: 'profile/:id',
    ORDERS_PARAM: 'orders/:customerId',

    nav: {
      root: () => ['/customers'],
      profile: (id: string) => ['/customers', 'profile', id],
      orders: (customerId: string) => ['/customers', 'orders', customerId]
    },

    url: {
      root: () => '/customers',
      profile: (id: string) => `/customers/profile/${id}`,
      orders: (customerId: string) => `/customers/orders/${customerId}`
    }
  }
} as const;
Enter fullscreen mode Exit fullscreen mode

Don't Make These Rookie Mistakes 🚫

1. Wrong Array Structure (Been There!)

// ❌ This will break your URLs
nav: {
  details: (id: string) => ['/products/details', id] // Nope!
}

// βœ… This is the way
nav: {
  details: (id: string) => ['/products', 'details', id] // Yes!
}
Enter fullscreen mode Exit fullscreen mode

2. Forgetting Type Safety

// ❌ Missing the magic
export const ROUTES = {
  // routes here
};

// βœ… TypeScript will love you for this
export const ROUTES = {
  // routes here
} as const;
Enter fullscreen mode Exit fullscreen mode

3. Inconsistent Naming

// ❌ Confusing mix
PRODUCTS: {
  nav: { /* navigation helpers */ },
  urls: { /* URL builders */ }, // Wait, is it 'url' or 'urls'?
}

// βœ… Consistent and clear
PRODUCTS: {
  nav: { /* navigation helpers */ },
  url: { /* URL builders */ },
}
Enter fullscreen mode Exit fullscreen mode

Migration Strategy (AKA "How to Fix Your Mess")

If you're stuck with an existing codebase full of hardcoded routes:

  1. Pick Your Battles - Start with your most-used feature (probably the one that changes most often)
  2. Create the Constants First - Define your route structure before touching components
  3. Replace Systematically - Use your IDE's find-and-replace, but test each change
  4. Test Everything - Seriously, test every navigation path
  5. Get Your Team On Board - Make sure everyone understands the new pattern

Pro tip: Use a regex search for common route patterns like ['/\w+ to find hardcoded navigation arrays.

The Bottom Line 🎯

This pattern has saved me countless hours and my sanity more times than I can count. Here's what you get:

βœ… One Change, Everywhere Updated - Change a route in one place, done

βœ… Type Safety - TypeScript catches route errors at compile time

βœ… Better DX - IntelliSense shows you available routes

βœ… Easier Testing - Predictable navigation patterns

βœ… Future Proof - Easy to extend as your app grows

βœ… Team Friendly - New developers immediately understand the pattern

This approach pairs beautifully with Angular 19's standalone components and modern DI patterns. Your future self will definitely thank you for this.

Want More Angular Magic? πŸͺ„

If this saved your day, you might also like my other posts about cleaning up Angular code:

Follow me for more Angular tips that'll make your code cleaner and your life easier! πŸš€


Have you tried this pattern? Got any horror stories about hardcoded routes? Drop them in the comments – misery loves company! πŸ˜„


Top comments (0)