When I first started working with Next.js, coming from years of experience with Laravel, I immediately missed one particular thing: named routes.
In Laravel, using named routes like route(‘dashboard.users.edit’, [‘id’ => 1])
was a dream. Clean, centralized, and safe. I never had to worry about writing URL strings manually or breaking links after a route changed.
But in Next.js, especially with the new App Router in Next 13+ (and now 15), this kind of system didn’t exist out of the box. You had to hardcode paths or rely on constants with plain strings, which felt messy and repetitive.
So I asked myself — why not bring Laravel’s route magic to Next.js?
This is how I built a custom route helper that gives me:
- Centralized named route definitions
- Support for nested and deeply nested routes
- Dynamic parameter replacement (e.g., :id)
- Full TypeScript support with autocomplete
Let’s dive in.
🧩 The Idea
We define all routes in a single object, grouped like in Laravel, and use a helper function to:
- Get the full URL by route name
- Replace any dynamic parameters
💡 Routes Definition (with nested groups and params)
// routes.ts
type Routes = {
[key: string]: string | Routes;
};
function prefixRoutes(basePath: string, routes: Routes): Routes {
const result: Routes = {};
for (const key in routes) {
const value = routes[key];
if (typeof value === 'string') {
result[key] = basePath + (value === '/' ? '' : value);
} else {
result[key] = prefixRoutes(
basePath + (key.startsWith('/') ? '' : '/' + key),
value
);
}
}
return result;
}
export const prefixedRoutes = {
dashboard: prefixRoutes('/dashboard', {
home: '/',
users: {
edit: '/users/:id/edit',
},
test: {
test1: {
something: '/test1/:somethingId/something',
},
},
}),
} as const;
This gives us route names like:
- dashboard.home ➜
/dashboard
- dashboard.users.edit ➜
/dashboard/users/:id/edit
- dashboard.test.test1.something ➜
/dashboard/test/test1/:somethingId/something
🧠 Route Helper Function
This function takes a route name like ‘dashboard.users.edit’ and optional parameters { id: 123 }, returning the final URL.
// route.ts
import { prefixedRoutes } from './routes';
export function route(name: string, params?: Record<string, string | number>): string {
const parts = name.split('.');
let current: unknown = prefixedRoutes;
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = (current as Record<string, unknown>)[part];
} else {
return '/';
}
}
if (typeof current !== 'string') return '/';
let url = current as string;
if (params) {
for (const key in params) {
url = url.replace(`:${key}`, String(params[key]));
}
}
return url;
}
✅ Usage Examples
import { route } from './route';
// 1. Simple route
const homeUrl = route('dashboard.home');
// → /dashboard
// 2. With dynamic param
const editUserUrl = route('dashboard.users.edit', { id: 123 });
// → /dashboard/users/123/edit
// 3. Deeply nested
const somethingUrl = route('dashboard.test.test1.something', { somethingId: 42 });
// → /dashboard/test/test1/42/something
You can use this route helper in router.push, Link, or href anywhere in your Next.js app.
<Link href={route('dashboard.users.edit', { id: user.id })}>
Edit User
</Link>
🧪 Type Safety and Improvements
This is just the beginning. You can expand it with:
- Stronger TypeScript typings (e.g. typed route names)
- Compile-time checks for missing params
- Query string support
(route(‘dashboard.users.edit’, { id: 1, tab: ‘profile’}))
🎯 Final Thoughts
This small system brought back all the joy and safety I had with Laravel routing, right into my Next.js project. No more hardcoded strings, no more guesswork.
If you’re working in a larger app with nested dashboards, user management, or any modular structure I highly recommend this approach. It’s simple, elegant, and familiar for Laravel developers.