Route::apiResource

Registers the five API CRUD routes, omitting the HTML create and edit forms.

Signature
apiResource(string $name, string $controller, array $options = []): Illuminate\Routing\PendingResourceRegistration

Parameters

ParameterTypeDescription
$$namestringThe resource name used for the URI segment and the route-name prefix; dot notation nests resources.
$$controllerstringThe controller class name. php artisan make:controller PhotoController --api generates one without create and edit methods.
$$optionsoptionalarray= []Registration options such as only, except, names, parameters and middleware, given as an array rather than chained.

Return value

Illuminate\Routing\PendingResourceRegistrationA PendingResourceRegistration supporting the same fluent modifiers as resource(): only(), except(), names(), parameters(), scoped(), shallow(), middleware(), middlewareFor(), withoutMiddlewareFor(), missing() and withTrashed().

Errors

Nothing at registration. At request time an unresolved implicit binding returns 404 unless missing() overrides it.

Route::apiResource is Route::resource with the two form-rendering routes removed. create and edit exist to serve HTML pages; a JSON API has no use for them, so it registers five routes:

VerbURIActionRoute Name
GET/photosindexphotos.index
POST/photosstorephotos.store
GET/photos/{photo}showphotos.show
PUT/PATCH/photos/{photo}updatephotos.update
DELETE/photos/{photo}destroyphotos.destroy

When to reach for it

Any controller in routes/api.php that maps to a model. Register several at once with Route::apiResources(['photos' => PhotoController::class, 'posts' => PostController::class]).

What trips people up

  • Route names are not namespaced for you. apiResource('photos', ...) in routes/api.php still names its routes photos.index, which collides with the web resource of the same name. Wrap it in Route::name('api.')->group(...).
  • The /api prefix comes from the file, not from this method — the same call in web.php produces unprefixed URIs.
  • update answers both PUT and PATCH, so a client sending PATCH lands in the same method and must be handled there.
  • Generate the matching controller with --api so it has no dead create/edit stubs.

Example

Example · php
<?php

// routes/api.php — the /api prefix is applied by the framework.
use App\Http\Controllers\Api\PhotoController;
use Illuminate\Support\Facades\Route;

Route::name('api.')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('photos', PhotoController::class);
});

echo route('api.photos.index');
echo PHP_EOL;
echo route('api.photos.show', ['photo' => 3]);
// http://example.com/api/photos
// http://example.com/api/photos/3

When to use it

  • Expose a model as a JSON API behind auth:sanctum in routes/api.php.
  • Register several API resources at once with Route::apiResources().
  • Drop destroy from a public API with ->except(['destroy']).
  • Nest comments under photos and scope the child binding by slug.

Frequently asked questions

What is the difference between apiResource and resource?

apiResource registers five routes instead of seven — it omits create and edit, which exist only to render HTML forms. Everything else, including the PUT/PATCH update route, is identical.

Does apiResource add the /api prefix?

No. That prefix comes from routes/api.php being loaded with an apiPrefix in bootstrap/app.php. The same apiResource call in web.php registers unprefixed URIs.

How do I stop my API route names clashing with my web route names?

Wrap the API routes in Route::name('api.')->group(...) so they become api.photos.index and so on. apiResource does not namespace names on its own.

More examples

Several resources, and trimming the set

apiResources registers two full five-route sets in one statement. middlewareFor is the finer tool: it attaches auth:sanctum to the three writing actions while index and show stay reachable without a token, which is otherwise two separate apiResource calls with only() and except().

Example · php
<?php

use App\Http\Controllers\PhotoController;
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::apiResources([
    'photos' => PhotoController::class,
    'posts'  => PostController::class,
]);

// A read-only endpoint: index and show only.
Route::apiResource('tags', TagController::class)->only(['index', 'show']);

// Require auth for writes but leave reads public.
Route::apiResource('comments', CommentController::class)
    ->middlewareFor(['store', 'update', 'destroy'], 'auth:sanctum');

echo route('tags.index');
echo PHP_EOL;
echo route('comments.store');
// http://example.com/api/tags
// http://example.com/api/comments

Nested API resource with scoped binding

scoped() does two things at once: it resolves the comment by its slug column instead of its id, and it constrains the lookup to the parent photo's comments relationship. A slug belonging to a different photo returns 404 rather than leaking the wrong record.

Example · php
<?php

use App\Http\Controllers\Api\PhotoCommentController;
use Illuminate\Support\Facades\Route;

Route::apiResource('photos.comments', PhotoCommentController::class)
    ->scoped(['comment' => 'slug']);

// Produces URIs such as:
//   GET    /api/photos/{photo}/comments
//   POST   /api/photos/{photo}/comments
//   GET    /api/photos/{photo}/comments/{comment:slug}
//   PUT    /api/photos/{photo}/comments/{comment:slug}
//   DELETE /api/photos/{photo}/comments/{comment:slug}

echo route('photos.comments.show', ['photo' => 3, 'comment' => 'nice-shot']);
// http://example.com/api/photos/3/comments/nice-shot

Discussion

  • Be the first to comment on this lesson.