When working with Laravel and Drupal, I noticed something striking: defining web routes feels vastly different between the two. Laravel offers what feels like an endless number of ways to register routes, while Drupal provides a single, standardized approach. Let me break this down.
1. How Laravel Defines Web Routes
In Laravel, routes for the web layer are typically defined in the routes/web.php file. But because of Laravel’s flexibility, there are many ways and places to register them:
- Classic approach:
Route::get('/articles', [ArticleController::class, 'index']);
- Using route groups with middleware, prefixes, and namespaces:
Route::prefix('admin')->middleware('auth')->group(function () {
Route::get('/dashboard', [AdminController::class, 'index']);
});
- RouteServiceProvider — loading routes from various files or programmatically:
$this->routes(function () {
Route::middleware('web')
->group(base_path('routes/web.php'));
});
- API routes, single-action controllers, resource controllers, closure routes, fallback routes…
Each method is slightly different, and Laravel doesn’t enforce a single “right” way. For someone coming from Drupal, this can feel overwhelming and inconsistent.
2. How Drupal Defines Routes
Drupal takes a completely different approach: everything is standardized through YAML files. For example:
example.content:
path: '/example'
defaults:
_controller: '\Drupal\example\Controller\ExampleController::content'
_title: 'Example page'
requirements:
_permission: 'access content'
- All routes live in a
MODULE_NAME.routing.ymlfile. - They are automatically loaded when the module is enabled.
- Middleware and permissions are also defined declaratively, either in the same file or related ones.
- There aren’t ten ways to do it — just one standard way.
3. Why Laravel Can Be Frustrating
- Too much freedom: No single “correct” method means every project might use routes differently. Tutorials and packages show varying patterns.
- Scattered logic: Middleware, service providers, resource routes, and more must all be remembered.
- Implicit conventions: Laravel relies heavily on naming conventions and “magic.” For example,
Route::resourcesilently creates 7 routes — you need to memorize them.
4. Why Drupal Feels “Happier”
- Strict standardization: One format (YAML), one place to define routes.
- Less magic: Open the
.routing.ymlfile, and you immediately see every URL, controller, and permission. - Predictable workflow: No surprises; modules and routes follow a consistent pattern.
Conclusion
Laravel’s flexibility is excellent for startups or highly customized solutions — but sometimes, all you want is standardization. Drupal enforces structure, which can feel restrictive, but also predictable and comforting.
If you value consistency and a single source of truth, Drupal’s routing system might just make you happier.
One Route, Many Ways: Why Laravel’s Routing Feels Chaotic Compared to Drupal
Let’s take a simple example: a route /articles that displays a list of articles via ArticleController@index. This is how it looks in both Laravel and Drupal — and why Laravel can feel overwhelming.
In Laravel: Multiple Ways to Define the Same Route
1. Directly in routes/web.php
Route::get('/articles', [ArticleController::class, 'index']);
2. Using an Anonymous Function
Route::get('/articles', function () {
return App\Models\Article::all();
});
3. Using a Route Group with Middleware
Route::middleware('auth')->group(function () {
Route::get('/articles', [ArticleController::class, 'index']);
});
4. Using a Prefix
Route::prefix('content')->group(function () {
Route::get('/articles', [ArticleController::class, 'index']);
});
5. Resource Route (Generates 7 Routes Automatically)
Route::resource('articles', ArticleController::class);
// GET /articles → index
// GET /articles/create → create
// POST /articles → store
// GET /articles/{id} → show
// etc.
6. In RouteServiceProvider
public function boot(): void
{
$this->routes(function () {
Route::middleware('web')
->group(base_path('routes/custom_routes.php'));
});
}
7. Using a Custom Route Macro
Route::macro('articles', function () {
Route::get('/articles', [ArticleController::class, 'index']);
});
Route::articles();
8. Single-Action Controller
// Controller has __invoke()
Route::get('/articles', ArticleController::class);
9. Fallback Route (Last Resort)
Route::fallback(function () {
return redirect('/articles');
});
And this is not all — you can also register routes via API route files, custom providers, dynamically loaded files, and more.
In Drupal: Only One Way
example.routing.yml:
example.articles:
path: '/articles'
defaults:
_controller: '\Drupal\example\Controller\ArticleController::index'
_title: 'Articles'
requirements:
_permission: 'access content'
- Automatically loaded when the module is enabled.
- Middleware (permissions) are declared in this file or in
.permissions.yml. - There are no alternative ways — and that’s actually a good thing for consistency.
Comparison
- Laravel: Gives you a dozen ways; flexibility can turn into chaos across different projects.
- Drupal: Enforces a strict YAML standard; predictable and stable, less flexible but easier to maintain.
Digging Deeper: Why Laravel’s Routing Feels Chaotic (and How to Make It More “Drupal-like”)
We’ve already looked at the common ways to define routes in Laravel. But let’s dig even deeper and explore all the remaining ways Laravel lets you register routes — to show exactly why it feels endless. Then, I’ll show you how to restrict it and create a “Drupal-like” structure for sanity.
Additional Routing Methods in Laravel
10. API Routes (routes/api.php)
Typically used for JSON endpoints, but you can also use them for web routes:
Route::middleware('api')->get('/articles', [ArticleController::class, 'index']);
By default, Laravel automatically prefixes them with /api, unless you remove the prefix.
11. Lazy-Loading Routes from Separate Files
You can store routes in module-specific files and load them dynamically:
Route::middleware('web')->group(base_path('modules/Blog/routes.php'));
12. Route Caching / Dynamic Loading
You can dynamically build routes based on database content:
$pages = Page::all();
foreach ($pages as $page) {
Route::get($page->slug, fn() => view('page', ['page' => $page]));
}
13. Custom RouteServiceProvider
You can create your own service provider that registers routes:
public function map()
{
Route::middleware('web')->group(base_path('routes/blog.php'));
}
14. Route Model Binding
Automatically inject models into routes:
Route::get('/articles/{article}', function (Article $article) {
return $article;
});
This is also a routing method — but with hidden “magic.”
15. Subdomain Routing
Route::domain('{account}.example.com')->group(function () {
Route::get('/articles', [ArticleController::class, 'index']);
});
16. Route Macros via Packages
Packages can add their own routing methods, for example:
Route::crud('articles');
(Common in tools like Backpack or Laravel Nova.)
17. Attribute Routing (Laravel 9+)
Using PHP attributes in controllers:
#[Route('/articles', methods: ['GET'])]
public function index() { ... }
18. Route::controller Grouping
Route::controller(ArticleController::class)->group(function () {
Route::get('/articles', 'index');
Route::post('/articles', 'store');
});
Why Drupal Feels Simpler
Drupal doesn’t have this problem: there’s one YAML file, one method, one entry point.
- No
RouteServiceProvider - No dynamic file loading
- No attributes or controller grouping inline
- Everything is predictable and consistent across every project
How to Make Laravel “Drupal-like”
If you want less chaos and more order in Laravel, here’s what you can do:
- Choose one format — for example, always define routes in
routes/web.php. - Avoid custom
RouteServiceProvidersetups — keep everything in one place. - Explicitly define all URLs — skip
resource,controller,fallback,apishortcuts. Create a YAML-like structure (custom loader):
- Store routes in
routes/routes.yml - Parse and load them at boot:
$routes = Yaml::parseFile(base_path('routes/routes.yml')); foreach ($routes as $route) { Route::{$route['method']}($route['path'], $route['action']); }This mimics Drupal’s approach.
- Store routes in
- Standardize middleware and prefixes — declare them at the top of the file, not scattered everywhere.
Even More Ways: Why Laravel’s Routing Feels “Endlessly Flexible” (vs Drupal’s Simplicity)
We’ve already covered many of Laravel’s routing options, but there are even more exotic and advanced methods found in real-world Laravel projects. Here’s the full picture — and why it makes Laravel feel scattered compared to Drupal.
19. Invokable Controllers with Route::controller()
A hybrid method using Route::controller with an __invoke action:
Route::controller(ArticleController::class)->group(function () {
Route::get('/articles', '__invoke');
});
20. Automatic Route Registration via Packages
Packages like Laravel Modules or Spatie Route Discovery can auto-scan controllers and register routes without explicitly declaring them:
// Spatie Route Discovery
// Automatically builds routes from attributes in controllers
21. Route Caching with Custom Preload
Used in large projects: routes are generated at runtime (e.g., from a database) and cached via php artisan route:cache.
22. Middleware-Defined Routes
Sometimes routes are defined inside middleware to handle access dynamically:
public function handle($request, Closure $next)
{
if ($request->user()->isAdmin()) {
Route::get('/admin-only', fn() => 'Secret');
}
return $next($request);
}
(Considered bad practice, but seen in legacy code.)
23. Route Binding via the Service Container
Routes can be registered inside service providers or packages:
$this->app->booted(function () {
Route::get('/articles', [ArticleController::class, 'index']);
});
24. Event-Based Routing
Register routes inside event listeners (e.g., when loading modules):
Event::listen('module.loaded', function () {
Route::get('/dynamic', fn() => 'Loaded dynamically');
});
25. Built-In Broadcast Routes (WebSockets)
Using Broadcast::routes():
Broadcast::routes(['middleware' => ['auth:api']]);
(Technically its own routing system.)
26. Laravel Livewire
Livewire components register routes automatically when mounting components:
Route::get('/articles', \App\Http\Livewire\Articles::class);
27. Custom Route Resolver via Kernel
You can override the RouteServiceProvider and build a custom resolver that loads routes from configs, JSON, or the database instead of files.
28. Multitenancy Routing
Using packages like tenancy/tenancy, routes are scoped per tenant:
Tenancy::tenantBootstrapped(function () {
Route::get('/dashboard', TenantDashboardController::class);
});
29. Dynamic Routing via Middleware
A highly flexible but confusing approach — building routes on-the-fly based on database content (like a CMS):
Route::get('/{slug}', [DynamicPageController::class, 'show']);
(In Drupal, this is handled automatically via the menu router.)
30. Inertia / SPA Wildcard Routing
For projects using Inertia.js or Vue/React SPA, Laravel routing often boils down to a single wildcard route:
Route::get('/{any}', fn() => Inertia::render('App'))->where('any', '.*');
In Total
Laravel truly offers 30+ ways to define routes. Many overlap or duplicate functionality, and new patterns emerge with each major package or architectural style. This flexibility is powerful — but can feel chaotic.
Drupal in Comparison
- One method:
MODULE_NAME.routing.yml - Occasionally, DynamicRouteSubscriber (rare)
- Transparent: path → controller → permission
- Even multisite setups are handled at the configuration level, not routing
Cheat Sheet: Laravel Routes vs Drupal Routing
1. routes/web.php (Basic Method)
Route::get('/articles', [ArticleController::class, 'index']);
Drupal: YAML:
example.articles:
path: '/articles'
defaults:
_controller: '\Drupal\example\Controller\ArticleController::index'
2. Closure Route
Route::get('/articles', fn() => 'Hello');
Drupal: No equivalent — controller required.
3. Route Group (middleware/prefix)
Route::middleware('auth')->prefix('admin')->group(function () {
Route::get('/articles', [AdminController::class, 'index']);
});
Drupal: Uses permissions in YAML.
4. Resource Route
Route::resource('articles', ArticleController::class);
Drupal: Each action must be defined separately in YAML.
5. API Routes (routes/api.php)
Route::middleware('api')->get('/articles', [ArticleController::class, 'index']);
Drupal: No API/Web split — all routes defined in a single routing.yml.
6. RouteServiceProvider
$this->routes(function () {
Route::middleware('web')->group(base_path('routes/custom.php'));
});
Drupal: Routes are auto-loaded from each module’s .routing.yml.
7. Single Action Controller (__invoke)
Route::get('/articles', ArticleController::class);
Drupal: Controllers always use explicit methods.
8. Route Macro
Route::macro('articles', fn() => Route::get('/articles', [ArticleController::class,'index']));
Route::articles();
Drupal: No equivalent.
9. Fallback Route
Route::fallback(fn() => redirect('/'));
Drupal: Uses page not found handler via event subscriber.
10. Lazy Loading (Separate Files)
Route::group([], base_path('routes/blog.php'));
Drupal: Not applicable — YAML is per module.
11. Dynamic Routes from Database
Page::all()->each(fn($p) => Route::get($p->slug, fn() => view('page', compact('p'))));
Drupal: Achieved via menu_links and entity system.
12. Custom RouteServiceProvider
class BlogRouteProvider extends ServiceProvider {
public function boot() {
Route::get('/blog', [BlogController::class, 'index']);
}
}
Drupal: Rarely via DynamicRouteSubscriber.
13. Route Model Binding
Route::get('/articles/{article}', fn(Article $article) => $article);
Drupal: Automatic parameter binding via entity type.
14. Subdomain Routing
Route::domain('{account}.example.com')->group(function () {
Route::get('/dashboard', DashboardController::class);
});
Drupal: Multisite configuration, not route-level.
15. Attribute Routing (PHP 8+)
#[Route('/articles', methods: ['GET'])]
public function index() { ... }
Drupal: Not available — YAML required.
16. Route::controller Grouping
Route::controller(ArticleController::class)->group(function () {
Route::get('/articles', 'index');
});
Drupal: No equivalent.
17. Broadcast Routes (WebSockets)
Broadcast::routes(['middleware' => ['auth:api']]);
Drupal: No direct equivalent.
18. Livewire Routes
Route::get('/articles', \App\Http\Livewire\Articles::class);
Drupal: Achieved via custom entity + AJAX, not automatic.
19. Event-Based Routing
Event::listen('booted', fn() => Route::get('/dynamic', fn() => 'Hello'));
Drupal: Rare, via kernel events.
20. Middleware-Defined Routes
public function handle($request, Closure $next)
{
Route::get('/secret', fn() => 'Secret');
return $next($request);
}
Drupal: Poor practice; possible via subscriber.
21. Multi-Tenancy Routes
Tenancy::tenantBootstrapped(function () {
Route::get('/dashboard', TenantDashboardController::class);
});
Drupal: Multisite + domain access module.
22. Inertia/SPA Wildcard
Route::get('/{any}', fn() => Inertia::render('App'))->where('any', '.*');
Drupal: Single controller + catch-all path.
23. Package-Based Auto Discovery (Spatie)
// Automatically scans controllers with attributes
Drupal: Not supported.
24. Route Binding via Services
$this->app->booted(fn() => Route::get('/bind', [Ctrl::class, 'method']));
Drupal: Always through YAML and container injection.
25. Route Caching
php artisan route:cache
Drupal: Automatic menu/route cache.
26. Route Discovery from Modules
(via laravel-modules package)
Module::loadRoutes('blog');
Drupal: Each module loads its YAML automatically.
27. SPA JSON Routes
Route::get('/articles', fn() => response()->json(Article::all()));
Drupal: JSON API module automatically generates endpoints.
28. Route::view Shortcut
Route::view('/welcome', 'welcome');
Drupal: Twig template via controller.
29. Route Constraints
Route::get('/user/{id}', fn($id) => $id)->whereNumber('id');
Drupal: Constraints via YAML requirements:
requirements:
id: \d+
30. Fallback Wildcard
Route::any('{any}', fn() => view('spa'))->where('any', '.*');
Drupal: page not found handler.
Comparison with Drupal
- Laravel: 30+ methods → flexibility, but chaos. Every project may define routes differently.
- Drupal: One main method (YAML), occasionally
DynamicRouteSubscriber. Minimal variations, strict structure.
Conclusion
Laravel offers unparalleled flexibility — over 30 ways to define routes. But this freedom can create inconsistency and cognitive load. Drupal, on the other hand, sticks to one YAML-based method, making projects more predictable and uniform.
What about you — what would make you happier?
Do you value Laravel’s freedom or Drupal’s clarity?
Write to me via the contact page →
(I’d love to hear your perspective and continue the conversation.)
Comments