Laravel gives you two common ways to handle route logic: write the logic directly inside a route closure, or point the route to a controller method. Both are valid. The important skill is knowing when each one keeps your application clearer.
This guide compares Laravel controllers vs closures from a practical developer point of view: readability, maintainability, testing, team development, route organization, and application size. If you are still getting comfortable with routes, read Laravel Routing Explained first. For broader context, see the Laravel Complete Guide and How to Install Laravel.
What Is a Closure Route in Laravel?
A closure route defines the request handling logic directly in the route file. In a typical Laravel application, browser-facing closure routes live in routes/web.php.
<?php
use IlluminateSupportFacadesRoute;
Route::get('/about', function () {
return view('pages.about');
});
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
Closure routes are direct and easy to read when the route does almost nothing. The route file shows the URL and the behavior in one place. That simplicity is their main advantage.
What Is a Laravel Controller?
A controller is a class that groups request handling logic into methods. Laravel stores controllers in app/Http/Controllers by default. Instead of putting logic inside the route file, the route points to a controller method.
php artisan make:controller UserController
<?php
namespace AppHttpControllers;
use AppModelsUser;
use IlluminateViewView;
class UserController extends Controller
{
public function show(User $user): View
{
return view('users.show', [
'user' => $user,
]);
}
}
use AppHttpControllersUserController;
use IlluminateSupportFacadesRoute;
Route::get('/users/{user}', [UserController::class, 'show'])
->name('users.show');
Controllers are common in real Laravel applications because they keep route files focused on URL structure while moving request handling into classes that are easier to organize, test, and extend.
Closure Routes vs Controller Routes
The choice is not about one being “professional” and the other being “wrong.” It is about matching the tool to the amount of behavior behind the route.
| Area | Closure routes | Controller routes |
|---|---|---|
| Simplicity | Excellent for tiny responses and prototypes. | Slightly more setup, better for real workflows. |
| Readability | Readable while the logic is short. | Readable when behavior grows beyond a few lines. |
| Maintainability | Can become messy if route files hold too much logic. | Easier to split, rename, refactor, and navigate. |
| Reusability | Usually tied to one route. | Can share dependencies, helper methods, form requests, and response patterns. |
| Testing | Often tested through feature tests. | Works well with feature tests and clearer class organization. |
| Application size | Good for very small apps and one-off routes. | Better as the app grows. |
| Separation of concerns | Weak if validation, queries, and decisions live in the route file. | Stronger when controllers coordinate requests and delegate deeper logic. |
| Team development | Can create crowded route files. | Gives teams clearer files and ownership boundaries. |
When Should You Use Closure Routes?
Use closure routes when the route is genuinely simple and likely to stay simple.
- Small static pages.
- Simple health checks.
- Temporary local development routes.
- Small prototypes.
- Very small redirects or JSON responses.
Route::get('/health', function () {
return response()->json([
'status' => 'ok',
]);
});
Route::view('/about', 'pages.about');
Route::redirect('/docs', '/documentation');
Do not use a closure route as a dumping ground for validation, database writes, authorization decisions, emails, events, and payment logic. Once a closure starts becoming a mini-application, move it.
When Should You Use Controllers?
Use controllers when a route represents real application behavior. That includes CRUD screens, authentication-related flows, blog dashboards, e-commerce pages, APIs, and anything with meaningful validation or business rules.
Route::get('/products', [ProductController::class, 'index'])
->name('products.index');
Route::post('/products', [ProductController::class, 'store'])
->middleware('auth')
->name('products.store');
Controllers do not mean “put everything in one method.” A controller should coordinate the HTTP request and response. As logic grows, it can call form requests, services, jobs, policies, models, and actions.
Creating a Controller
Create a controller with Artisan:
php artisan make:controller UserController
A realistic controller method should be focused. This example loads a user and recent published posts for a profile page.
<?php
namespace AppHttpControllers;
use AppModelsUser;
use IlluminateViewView;
class UserController extends Controller
{
public function show(User $user): View
{
$user->load(['posts' => function ($query) {
$query->where('is_published', true)
->latest()
->limit(5);
}]);
return view('users.show', compact('user'));
}
}
If that query or profile-building logic becomes complex, extract it. The controller should not become the place where every business rule goes to hide.
Connecting Routes to Controllers
Laravel controller routes use an array syntax: controller class first, method name second.
use AppHttpControllersPostController;
Route::get('/posts', [PostController::class, 'index'])
->name('posts.index');
Route::get('/posts/{post:slug}', [PostController::class, 'show'])
->name('posts.show');
Route::post('/posts', [PostController::class, 'store'])
->middleware('auth')
->name('posts.store');
The {post:slug} route parameter uses route model binding to resolve a post by slug. Route parameters are passed to the matching controller method.
public function show(Post $post): View
{
return view('posts.show', [
'post' => $post,
]);
}
Resource Controllers
A resource controller is useful when a controller manages a typical CRUD resource such as products, posts, users, orders, or projects.
php artisan make:controller ProductController --resource
use AppHttpControllersProductController;
Route::resource('products', ProductController::class);
A resource route creates the standard controller actions:
index: list resources.create: show a creation form.store: save a new resource.show: display one resource.edit: show an edit form.update: save changes.destroy: delete the resource.
You can keep a resource controller partial if the application does not need every action.
Route::resource('products', ProductController::class)
->only(['index', 'show', 'store']);
Route::resource('orders', OrderController::class)
->except(['destroy']);
Single-Action Controllers
A single-action controller has one __invoke method. It is useful when an action deserves its own class but does not naturally belong in a multi-method controller.
php artisan make:controller PublishPostController --invokable
<?php
namespace AppHttpControllers;
use AppModelsPost;
use IlluminateHttpRedirectResponse;
class PublishPostController extends Controller
{
public function __invoke(Post $post): RedirectResponse
{
$post->update(['is_published' => true]);
return redirect()->route('posts.show', $post);
}
}
use AppHttpControllersPublishPostController;
Route::post('/posts/{post}/publish', PublishPostController::class)
->middleware('auth')
->name('posts.publish');
Invokable controllers are especially nice for focused actions such as publishing a post, exporting a report, sending an invitation, or retrying a failed job.
Keeping Business Logic Out of Controllers
Controllers should usually handle the HTTP layer: accept the request, authorize when needed, validate or delegate validation, call application behavior, and return a response. They should not become enormous classes containing every query, rule, notification, and side effect.
As logic grows, move it into appropriate places:
- Form Requests for validation and request authorization.
- Services or actions for workflows that do not belong directly in a model.
- Jobs for background work.
- Policies for authorization rules.
- Models for model-specific relationships, casts, scopes, and domain behavior.
Related reading: Laravel Routing Explained and Laravel Migrations Best Practices.
Common Mistakes
- Putting large amounts of logic directly inside route closures.
- Creating controllers for routes that only return a simple view.
- Letting one controller method handle validation, authorization, queries, payments, emails, and response formatting.
- Using controller names that do not describe the resource or action.
- Creating one giant controller for unrelated features.
- Hardcoding URLs instead of using named routes.
- Overengineering a tiny application before the structure is needed.
- Keeping temporary development routes in production route files.
Best Practices
Use a closure when the route is tiny, obvious, and unlikely to grow.
- The route returns a static view.
- The route returns a small health check response.
- The route is temporary and local-only.
- The route has no meaningful business rules.
Use a controller when the route has real application behavior.
- The route reads or writes database records.
- The route needs validation or authorization.
- The route belongs to a CRUD resource.
- The route returns API responses.
- The route will be maintained by a team.
- The logic may be reused or tested in a structured way.
A practical default: closures are fine for small edges of the application; controllers should handle the core product behavior.
Real-World Example
Imagine a small product catalog application. Some routes are simple enough for closures or view routes. Others clearly belong in controllers.
use AppHttpControllersProductController;
use AppHttpControllersProductExportController;
use IlluminateSupportFacadesRoute;
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
Route::view('/about', 'pages.about')->name('about');
Route::get('/products', [ProductController::class, 'index'])
->name('products.index');
Route::get('/products/{product:slug}', [ProductController::class, 'show'])
->name('products.show');
Route::post('/products', [ProductController::class, 'store'])
->middleware('auth')
->name('products.store');
Route::post('/products/export', ProductExportController::class)
->middleware('auth')
->name('products.export');
The health route is a closure because it returns a tiny response. The about page can be a view route because it only returns a Blade view. Product listing, product detail, product creation, and product export belong in controllers because they involve real application behavior.
Frequently Asked Questions
Are Laravel closure routes bad?
No. Closure routes are useful for simple routes. They become a problem when they hold complex behavior that should be organized elsewhere.
Are controllers faster than closures?
For most applications, choose based on maintainability and structure, not assumed speed. The difference that usually matters is clean organization, correct caching, efficient queries, and avoiding unnecessary work.
Should every Laravel route use a controller?
No. Static pages, simple redirects, health checks, and tiny prototype routes can be closures or view routes. Core application behavior usually belongs in controllers.
Can I use middleware with controller routes?
Yes. You can attach middleware directly to controller routes or route groups.
Route::get('/profile', [UserController::class, 'show'])
->middleware('auth');
Can a controller method receive route parameters?
Yes. Route parameters are passed to controller methods. You can receive raw values or use route model binding.
Route::get('/users/{user}', [UserController::class, 'show']);
public function show(User $user): View
{
return view('users.show', compact('user'));
}
When should I refactor a closure into a controller?
Refactor when the closure starts doing validation, authorization, database writes, multiple queries, notifications, external API calls, or anything that makes the route file harder to scan.
Conclusion
Laravel closure routes are excellent for simple edges of an application. Controllers are better for the parts of the application that contain real behavior, grow over time, need route parameters, use middleware, touch the database, or require clearer organization.
The practical recommendation is simple: use closures for tiny routes that are obvious at a glance, and use controllers for product behavior. That gives you the speed of Laravel’s expressive routing without turning your route files into a maintenance problem.
FAQ
Are Laravel closure routes bad?
No. Closure routes are useful for simple routes. They become a problem when they hold complex behavior that should be organized elsewhere.
Are controllers faster than closures?
For most applications, choose based on maintainability and structure, not assumed speed. Clean organization, efficient queries, and correct caching usually matter more.
Should every Laravel route use a controller?
No. Static pages, simple redirects, health checks, and tiny prototype routes can be closures or view routes. Core application behavior usually belongs in controllers.
Can I use middleware with controller routes?
Yes. You can attach middleware directly to controller routes or route groups.
Can a controller method receive route parameters?
Yes. Route parameters are passed to controller methods. You can receive raw values or use route model binding.
When should I refactor a closure into a controller?
Refactor when the closure starts doing validation, authorization, database writes, multiple queries, notifications, external API calls, or anything that makes the route file harder to scan.
