API Routes
routes/api.php arrives with php artisan install:api, carries the /api prefix, and runs stateless under the api middleware group.
php artisan install:api # creates routes/api.php and installs SanctumAn API route answers with data instead of a page. In Laravel 13 a fresh application ships no routes/api.php at all. The php artisan install:api command creates it from a stub, installs Laravel Sanctum, publishes the personal access tokens migration, offers to run your pending migrations, and uncomments the api: argument in bootstrap/app.php so the file is loaded.
Two things happen off-screen
Nothing in routes/api.php says /api or names any middleware, yet every route in it gets both. The URI prefix comes from the apiPrefix argument of withRouting() in bootstrap/app.php, which defaults to api. The api middleware group is applied to the whole file. In Laravel 13 that group holds one middleware, Illuminate\Routing\Middleware\SubstituteBindings, the one that turns {post} in a URI into a Post model.
Stateless
No session middleware runs, so there is no session, no flash data, no CSRF token and nothing to redirect back to. Each request carries its own credentials. Rate limiting is not on by default either: $middleware->throttleApi() adds throttle:api to the group.
Shorthand
Route::apiResource('posts', PostController::class) is shorthand. It registers the same five routes you would write by hand with Route::get(), Route::post(), Route::put() and Route::delete(), and skips the create and edit routes of Route::resource(), which exist only to render HTML forms.
Example
<?php
// routes/api.php
use App\Http\Controllers\Api\PostController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// The route install:api publishes. Answers at GET /api/user.
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
// Shorthand. Five routes, one line.
Route::apiResource('posts', PostController::class);
// Longhand. Exactly what the line above registered:
//
// Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
// Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
// Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
// Route::match(['put', 'patch'], '/posts/{post}', [PostController::class, 'update'])
// ->name('posts.update');
// Route::delete('/posts/{post}', [PostController::class, 'destroy'])
// ->name('posts.destroy');
// Routes that need a token. The group applies the guard, not the file.
Route::middleware('auth:sanctum')->group(function (): void {
Route::post('/posts/{post}/publish', [PostController::class, 'publish'])
->name('posts.publish');
});
When to use it
- Serve a mobile client the same records your Blade pages already render.
- Accept a webhook from a payment provider that cannot hold a session cookie.
- Put a JavaScript front end on its own domain in front of an existing application.
- Expose an existing resource controller as JSON without its create and edit routes.
More examples
Where the prefix and the middleware group are configured
Both pieces of magic in an API route live in this one file. If an API route 404s on a URI you are sure you registered, or a middleware you expected is missing, read bootstrap/app.php before you read the route file.
<?php
// bootstrap/app.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',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
// Default is 'api'. Change it and every API route moves.
apiPrefix: 'api',
)
->withMiddleware(function (Middleware $middleware): void {
// Adds 'throttle:api' to the api group. Not there by default.
$middleware->throttleApi();
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
The controller make:controller --api generates
The --api switch omits the create and edit methods, so the controller matches what apiResource registers. The --model switch type-hints the model, which is what makes route model binding resolve {post} for you.
<?php
// app/Http/Controllers/Api/PostController.php
// Generated by: php artisan make:controller Api/PostController --api --model=Post
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
//
}
public function store(Request $request)
{
//
}
// $post is resolved by SubstituteBindings from the {post} URI segment.
public function show(Post $post)
{
//
}
public function update(Request $request, Post $post)
{
//
}
public function destroy(Post $post)
{
//
}
}
Confirming what is registered
Reach for this whenever a request does not land where you expect. -v prints the middleware and group names per route, -vv expands each group into its individual middleware, so you can see whether throttle:api and auth:sanctum are really applied.
php artisan route:list --path=api -v
# GET|HEAD api/posts ............ posts.index > Api\PostController@index
# api
# POST api/posts ............ posts.store > Api\PostController@store
# api
# GET|HEAD api/posts/{post} ..... posts.show > Api\PostController@show
# api
# Expand the group to see the middleware inside it.
php artisan route:list --path=api -vv
Discussion