Laravel 13 applications do not ship with a RouteServiceProvider. Route files are registered in bootstrap/app.php through withRouting(): use its then closure to add extra route files such as routes/admin.php, or the using closure to take full control. Rate limiters and custom route model bindings, which used to live in the provider, now go in the boot() method of AppServiceProvider.
The provider was removed from the application skeleton in Laravel 11 as part of the slimmer structure, and Laravel 13 keeps that layout. If you are upgrading an older project, your existing provider still works; this guide is for new projects or for teams moving to the new style.
Prerequisites
- Laravel 13.x on PHP 8.3 or newer.
- Basic familiarity with route groups, middleware and named routes.
Where are routes registered in Laravel 13?
Open bootstrap/app.php. A fresh project contains something like this:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();web loads routes/web.php with the web middleware group, commands loads console routes, and health registers a simple health-check endpoint at /up for load balancers and uptime monitors.
Step 1: Add API routes
API routes are not installed by default. Add them with one command:
php artisan install:apiIt creates routes/api.php, installs Laravel Sanctum, and adds api: __DIR__.'/../routes/api.php' to withRouting(). Those routes get the api middleware group and the /api prefix. To change the prefix, add apiPrefix: 'api/v1' next to the api argument.
Step 2: Register a custom route file
Large apps often split routes by area. Create routes/admin.php and register it inside the then closure, which runs after the default files are loaded:
use Illuminate\Support\Facades\Route;
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware(['web', 'auth'])
->prefix('admin')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::middleware('api')
->prefix('webhooks')
->name('webhooks.')
->group(base_path('routes/webhooks.php'));
},
)Inside routes/admin.php you write plain routes. The prefix, name prefix and middleware are applied automatically:
<?php
use App\Http\Controllers\Admin\UserController;
use Illuminate\Support\Facades\Route;
// URL: /admin/users, name: admin.users.index
Route::get('/users', [UserController::class, 'index'])->name('users.index');Webhook routes use the api group on purpose: they are stateless and should not need a CSRF token.
Step 3: Take full control with the using closure
If you prefer to register every route file yourself, pass using instead of web and api. Laravel then registers no HTTP routes on its own:
->withRouting(
commands: __DIR__.'/../routes/console.php',
using: function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
},
)I only reach for this when route registration depends on runtime conditions, for example a multi-tenant app that loads different files per domain.
Where do rate limiters go now?
Define them in App\Providers\AppServiceProvider::boot():
<?php
namespace App\Providers;
use App\Models\Post;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Custom route model binding: resolve {post} by slug.
Route::bind('post', fn (string $value) => Post::where('slug', $value)->firstOrFail());
// Global constraint: every {id} parameter must be numeric.
Route::pattern('id', '[0-9]+');
}
}Attach a limiter with ->middleware('throttle:api') on a route or group. For most slug lookups you do not even need Route::bind: Route::get('/posts/{post:slug}', ...) tells implicit binding which column to use.
How do I organize routes in a large Laravel 13 app?
A layout that scales well is one file per audience: web.php for public and customer pages, admin.php for the back office, api.php for the public API and webhooks.php for third-party callbacks. Each file gets its own middleware, prefix and name prefix in bootstrap/app.php, so a developer opening admin.php knows every route there is already authenticated. Inside a file, group related routes with Route::controller() or Route::resource() rather than repeating the controller class on every line. For versioned APIs, a routes/api/v1.php and routes/api/v2.php pair registered with different prefixes keeps old clients working while new endpoints evolve.
Common mistakes
- Creating a RouteServiceProvider out of habit. It is not needed. If you do add one, register it in
bootstrap/providers.php, or it never runs. - Missing the
webgroup on custom files. Without it, sessions, CSRF protection and error bags do not work for browser routes. - Using
thenandusingtogether.usingreplaces default registration, so mixing the two leads to confusion. Pick one style. - Stale route cache. After changing route files in production, run
php artisan route:cacheagain as part of deployment. - Closures in routes with route caching. Modern Laravel can cache closure routes, but controller routes are easier to test and reuse.
How to verify it works
List the routes and filter by path or name:
php artisan route:list --path=admin
php artisan route:list --name=webhooksCheck the middleware column to confirm the groups you expect. Add a quick feature test such as $this->get('/admin/users')->assertRedirect('/login') for a guest to prove the auth middleware is active, and request /up to see the health endpoint return 200.
FAQ
Can I still use a RouteServiceProvider after upgrading?
Yes. Upgraded apps can keep their provider. The new structure is recommended for new code, not forced on old projects.
Where do I set the HOME redirect constant?
There is no HOME constant any more. Redirect to a named route directly, and configure where authenticated users are sent from guest pages with $middleware->redirectUsersTo().
How do I add a subdomain route group?
Use Route::domain('{account}.example.com')->group(...) in a route file, or inside the then closure.



