Skip to main content
api-design Design RESTful APIs with Laravel following best practices for resource naming, HTTP methods, status codes, pagination, filtering, error responses, and versioning. Use when creating API endpoints, designing response formats, implementing pagination or filtering, handling API errors, or versioning your API. Also covers Inertia route conventions for monolith apps. Triggers on API endpoint, REST, route design, pagination, filtering, error response, or CORS.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/bramato/laravel-react-plugins --skill api-design命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 同仓库更多 Skills Implement authentication with Laravel Sanctum and authorization with Policies and Gates. Use when setting up login/registration, implementing API token auth, SPA cookie auth, creating policies, defining gates, or building role-based access control. Triggers on authentication, authorization, login, Sanctum, policy, gate, RBAC, permission, or middleware.
Design database schemas with Laravel migrations, Eloquent relationships, and indexing strategies. Use when creating migrations, designing table schemas, defining relationships between models, optimizing queries with indexes, or planning database architecture. Triggers on migration, database schema, table design, foreign key, index, relationship, Eloquent, or query optimization.
Use when setting up deployment pipelines, Docker environments, CI/CD, or server configuration for Laravel + React applications. Covers Docker Compose, GitHub Actions, Laravel Forge, Laravel Vapor, zero-downtime deployments, and monitoring.
name api-design description Design RESTful APIs with Laravel following best practices for resource naming, HTTP methods, status codes, pagination, filtering, error responses, and versioning. Use when creating API endpoints, designing response formats, implementing pagination or filtering, handling API errors, or versioning your API. Also covers Inertia route conventions for monolith apps. Triggers on API endpoint, REST, route design, pagination, filtering, error response, or CORS.
API Design with Laravel
Laravel supports two distinct routing strategies depending on the application architecture.
Choose the right one based on your delivery target.
Route Conventions
Two Modes of Operation
Inertia routes (default for monolith) — defined in routes/web.php, render Inertia
pages via Inertia::render(). The browser receives a full React page.
API routes (for mobile / external clients) — defined in routes/api.php, return
pure JSON. Authenticated via Sanctum tokens.
Both can coexist in the same application. Inertia controllers return Inertia::render()
while API controllers return response()->json().
Inertia Route Patterns
Basic Resource Routes
use App \Http \Controllers \OrderController ;
Route ::middleware (['auth' , 'verified' ])->group (function () {
Route :: ( , :: );
});
resource
'orders'
OrderController
class
The Route::resource() macro generates all seven RESTful routes:
Verb URI Action Route Name GET /orders index orders.index GET /orders/create create orders.create POST /orders store orders.store GET /orders/{order} show orders.show GET /orders/{order}/edit edit orders.edit PUT/PATCH /orders/{order} update orders.update DELETE /orders/{order} destroy orders.destroy
Inertia Controller Example namespace App \Http \Controllers ;
use App \Models \Order ;
use App \Http \Requests \StoreOrderRequest ;
use App \Http \Requests \UpdateOrderRequest ;
use Illuminate \Http \RedirectResponse ;
use Inertia \Inertia ;
use Inertia \Response ;
class OrderController extends Controller
{
public function index ( ): Response
{
return Inertia ::render ('Orders/Index' , [
'orders' => Order ::query ()
->where ('user_id' , auth ()->id ())
->with ('customer:id,name' )
->latest ()
->paginate (25 ),
]);
}
public function create ( ): Response
{
return Inertia ::render ('Orders/Create' , [
'customers' => Customer ::select ('id' , 'name' )->get (),
]);
}
public function store (StoreOrderRequest $request ): RedirectResponse
{
$order = Order ::create ($request ->validated ());
return redirect ()
->route ('orders.show' , $order )
->with ('success' , 'Order created successfully.' );
}
public function show (Order $order ): Response
{
$this ->authorize ('view' , $order );
return Inertia ::render ('Orders/Show' , [
'order' => $order ->load ('items.product' , 'customer' ),
]);
}
public function edit (Order $order ): Response
{
$this ->authorize ('update' , $order );
return Inertia ::render ('Orders/Edit' , [
'order' => $order ->load ('items' ),
'customers' => Customer ::select ('id' , 'name' )->get (),
]);
}
public function update (UpdateOrderRequest $request , Order $order ): RedirectResponse
{
$order ->update ($request ->validated ());
return redirect ()
->route ('orders.show' , $order )
->with ('success' , 'Order updated successfully.' );
}
public function destroy (Order $order ): RedirectResponse
{
$this ->authorize ('delete' , $order );
$order ->delete ();
return redirect ()
->route ('orders.index' )
->with ('success' , 'Order deleted successfully.' );
}
}
Nested Resources Route ::resource ('orders.items' , OrderItemController ::class )->scoped ();
Generates routes like /orders/{order}/items/{item}. The scoped() call enforces
that the item belongs to the given order via implicit model binding.
Partial Resources
Route ::resource ('orders' , OrderController ::class )->only (['index' , 'show' ]);
Route ::resource ('orders' , OrderController ::class )->except (['destroy' ]);
Route Naming Conventions
Use plural nouns for resources: orders, users, products
Nested names follow dot notation: orders.items.index
Custom action routes use verbs: orders.cancel, orders.export
API Route Patterns
Basic API Resource Routes
use App \Http \Controllers \Api \V1 \OrderController ;
Route ::prefix ('v1' )->middleware ('auth:sanctum' )->group (function () {
Route ::apiResource ('orders' , OrderController ::class );
Route ::post ('orders/{order}/cancel' , [OrderController ::class , 'cancel' ]);
});
Route::apiResource() generates five routes (excludes create and edit since APIs
do not serve HTML forms):
Verb URI Action Route Name GET /api/v1/orders index orders.index POST /api/v1/orders store orders.store GET /api/v1/orders/{order} show orders.show PUT/PATCH /api/v1/orders/{order} update orders.update DELETE /api/v1/orders/{order} destroy orders.destroy
API Controller Example namespace App \Http \Controllers \Api \V1 ;
use App \Http \Controllers \Controller ;
use App \Http \Requests \StoreOrderRequest ;
use App \Http \Requests \UpdateOrderRequest ;
use App \Http \Resources \OrderResource ;
use App \Http \Resources \OrderCollection ;
use App \Models \Order ;
use Illuminate \Http \JsonResponse ;
class OrderController extends Controller
{
public function index ( ): OrderCollection
{
$orders = Order ::query ()
->where ('user_id' , auth ()->id ())
->with ('customer:id,name' )
->latest ()
->paginate (25 );
return new OrderCollection ($orders );
}
public function store (StoreOrderRequest $request ): JsonResponse
{
$order = Order ::create ($request ->validated ());
return (new OrderResource ($order ))
->response ()
->setStatusCode (201 );
}
public function show (Order $order ): OrderResource
{
$this ->authorize ('view' , $order );
return new OrderResource ($order ->load ('items.product' , 'customer' ));
}
public function update (UpdateOrderRequest $request , Order $order ): OrderResource
{
$this ->authorize ('update' , $order );
$order ->update ($request ->validated ());
return new OrderResource ($order ->fresh ());
}
public function destroy (Order $order ): JsonResponse
{
$this ->authorize ('delete' , $order );
$order ->delete ();
return response ()->json (null , 204 );
}
public function cancel (Order $order ): OrderResource
{
$this ->authorize ('cancel' , $order );
$order ->markAsCancelled ();
return new OrderResource ($order ->fresh ());
}
}
RESTful Resource Naming
URL Structure Rules
Use plural nouns for collections: /orders, /users, /products
Use IDs for individual resources: /orders/{order}
Nest only one level deep: /orders/{order}/items
Use POST with verbs for non-CRUD actions: /orders/{order}/cancel
Avoid deeply nested routes; use query parameters instead:
Bad: /users/{user}/orders/{order}/items/{item}/reviews
Good: /reviews?order_item_id=42
Common Patterns
Route ::apiResource ('products' , ProductController ::class );
Route ::apiResource ('orders.items' , OrderItemController ::class )->scoped ();
Route ::post ('orders/{order}/cancel' , [OrderController ::class , 'cancel' ]);
Route ::post ('orders/{order}/ship' , [OrderController ::class , 'ship' ]);
Route ::get ('orders/{order}/invoice' , [OrderController ::class , 'invoice' ]);
Route ::apiSingleton ('profile' , ProfileController ::class );
HTTP Methods & Status Codes
Complete Reference Table Method Purpose Request Body Idempotent Laravel Response Helper GET Retrieve resource(s) No Yes response()->json($data)POST Create resource Yes No response()->json($data, 201)PUT Full update Yes Yes response()->json($data)PATCH Partial update Yes Yes response()->json($data)DELETE Remove resource No Yes response()->json(null, 204)
Status Codes Code Meaning When to Use 200 OK Successful GET, PUT, PATCH 201 Created Successful POST that creates a resource 204 No Content Successful DELETE 301 Moved Permanently Resource URL changed permanently 302 Found Redirect after Inertia form submission 400 Bad Request Malformed request syntax 401 Unauthorized Missing or invalid authentication 403 Forbidden Authenticated but not authorized 404 Not Found Resource does not exist 409 Conflict Resource state conflict (e.g., duplicate) 422 Unprocessable Entity Validation errors 429 Too Many Requests Rate limit exceeded 500 Server Error Unexpected server failure
Request/Response Format
JSON Response Structure (Flat Style — Recommended) {
"data" : {
"id" : 1 ,
"order_number" : "ORD-20240101-001" ,
"status" : "pending" ,
"total" : "149.99" ,
"customer" : {
"id" : 5 ,
"name" : "Jane Doe"
} ,
"created_at" : "2024-01-15T10:30:00Z"
}
}
Collection with pagination:
{
"data" : [
{ "id" : 1 , "order_number" : "ORD-001" , "status" : "pending" } ,
{ "id" : 2 , "order_number" : "ORD-002" , "status" : "shipped" }
] ,
"links" : {
"first" : "/api/v1/orders?page=1" ,
"last" : "/api/v1/orders?page=5" ,
"prev" : null ,
"next" : "/api/v1/orders?page=2"
} ,
"meta" : {
"current_page" : 1 ,
"last_page" : 5 ,
"per_page" : 25 ,
"total" : 120
}
}
API Resource Classes namespace App \Http \Resources ;
use Illuminate \Http \Request ;
use Illuminate \Http \Resources \Json \JsonResource ;
class OrderResource extends JsonResource
{
public function toArray (Request $request ): array
{
return [
'id' => $this ->id,
'order_number' => $this ->order_number,
'status' => $this ->status,
'total' => $this ->total,
'customer' => new CustomerResource ($this ->whenLoaded ('customer' )),
'items' => OrderItemResource ::collection ($this ->whenLoaded ('items' )),
'items_count' => $this ->whenCounted ('items' ),
'created_at' => $this ->created_at->toISOString (),
'updated_at' => $this ->updated_at->toISOString (),
];
}
}
Pagination
Offset Pagination (Standard)
$orders = Order ::query ()->latest ()->paginate (25 );
return new OrderCollection ($orders );
Best for: admin dashboards, traditional page-based navigation, when total count matters.
Cursor Pagination (High Performance) $orders = Order ::query ()->latest ()->cursorPaginate (25 );
return new OrderCollection ($orders );
Best for: infinite scroll, real-time feeds, very large datasets.
Cursor pagination does not count total rows, making it significantly faster on large tables.
Customizing Per-Page
$perPage = min ($request ->integer ('per_page' , 25 ), 100 );
$orders = Order ::paginate ($perPage );
Filtering & Sorting
Query Parameter Conventions GET /api/v1/orders?filter[status]=pending&filter[customer_id]=5&sort=-created_at&per_page=25
filter[field]=value for filtering
sort=field for ascending, sort=-field for descending
Multiple sort fields: sort=-created_at,order_number
Manual Filter Implementation public function index (Request $request ): OrderCollection
{
$query = Order ::query ()->where ('user_id' , auth ()->id ());
if ($status = $request ->input ('filter.status' )) {
$query ->where ('status' , $status );
}
if ($customerId = $request ->input ('filter.customer_id' )) {
$query ->where ('customer_id' , $customerId );
}
if ($search = $request ->input ('filter.search' )) {
$query ->where ('order_number' , 'like' , "%{$search} %" );
}
$sortField = ltrim ($request ->input ('sort' , '-created_at' ), '-' );
$sortDirection = str_starts_with ($request ->input ('sort' , '-created_at' ), '-' ) ? 'desc' : 'asc' ;
$allowedSorts = ['created_at' , 'total' , 'status' , 'order_number' ];
if (in_array ($sortField , $allowedSorts )) {
$query ->orderBy ($sortField , $sortDirection );
}
return new OrderCollection ($query ->paginate (25 ));
}
Spatie QueryBuilder Integration use Spatie \QueryBuilder \QueryBuilder ;
use Spatie \QueryBuilder \AllowedFilter ;
public function index ( ): OrderCollection
{
$orders = QueryBuilder ::for (Order ::class )
->allowedFilters ([
AllowedFilter ::exact ('status' ),
AllowedFilter ::exact ('customer_id' ),
AllowedFilter ::scope ('date_range' ),
AllowedFilter ::partial ('order_number' ),
])
->allowedSorts (['created_at' , 'total' , 'order_number' ])
->allowedIncludes (['customer' , 'items' ])
->defaultSort ('-created_at' )
->paginate (25 );
return new OrderCollection ($orders );
}
Error Response Format
Standard Laravel Validation Error (422) {
"message" : "The given data was invalid." ,
"errors" : {
"email" : [
"The email field is required." ,
"The email must be a valid email address."
] ,
"name" : [
"The name field is required."
]
}
}
Not Found Error (404)
public function show (Order $order ): OrderResource { ... }
abort (404 , 'Order not found.' );
{
"message" : "Order not found."
}
Unauthorized (401) vs Forbidden (403)
401 Unauthorized : The client has not authenticated. Sanctum returns this when no valid
token/session is provided.
403 Forbidden : The client is authenticated but does not have permission. Return this
from policy checks.
$this ->authorize ('update' , $order );
abort (403 , 'You do not own this order.' );
Custom Exception Handler
use Illuminate \Foundation \Configuration \Exceptions ;
use Symfony \Component \HttpKernel \Exception \NotFoundHttpException ;
->withExceptions (function (Exceptions $exceptions ) {
$exceptions ->render (function (NotFoundHttpException $e ) {
if (request ()->expectsJson ()) {
return response ()->json ([
'message' => 'Resource not found.' ,
], 404 );
}
});
})
API Versioning
URL Prefix Versioning (Recommended)
Route ::prefix ('v1' )->group (function () {
Route ::apiResource ('orders' , Api\V1\OrderController ::class );
});
Route ::prefix ('v2' )->group (function () {
Route ::apiResource ('orders' , Api\V2\OrderController ::class );
});
app/Http/Controllers/Api/
V1/
OrderController.php
V2/
OrderController.php
app/Http/Resources/
V1/
OrderResource.php
V2/
OrderResource.php
When to Version
Breaking changes to response structure
Removing fields from responses
Changing field types or semantics
Do NOT version for additive changes (new optional fields)
Rate Limiting
Default Throttle Middleware
Route ::middleware ('throttle:api' )->group (function () {
Route ::apiResource ('orders' , OrderController ::class );
});
Custom Rate Limiters
use Illuminate \Cache \RateLimiting \Limit ;
use Illuminate \Support \Facades \RateLimiter ;
->withMiddleware (function (Middleware $middleware ) {
})
->booted (function () {
RateLimiter ::for ('api' , function (Request $request ) {
return Limit ::perMinute (60 )->by ($request ->user ()?->id ?: $request ->ip ());
});
RateLimiter ::for ('uploads' , function (Request $request ) {
return Limit ::perMinute (10 )->by ($request ->user ()->id);
});
})
Route ::post ('uploads' , [UploadController ::class , 'store' ])
->middleware ('throttle:uploads' );
Rate limit headers are automatically included: X-RateLimit-Limit, X-RateLimit-Remaining,
Retry-After.
CORS Configuration
SPA Mode (API consumed by a separate frontend domain)
return [
'paths' => ['api/*' , 'sanctum/csrf-cookie' ],
'allowed_methods' => ['*' ],
'allowed_origins' => [env ('FRONTEND_URL' , 'http://localhost:3000' )],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*' ],
'exposed_headers' => [],
'max_age' => 0 ,
'supports_credentials' => true ,
];
Inertia Monolith CORS configuration is not needed for Inertia applications because the frontend and
backend share the same origin. All requests are same-origin by definition.
Authentication
Sanctum SPA Authentication (Cookie-Based, for Inertia) This is the default for Laravel + Inertia. Session-based auth with CSRF protection.
Route ::middleware (['auth' , 'verified' ])->group (function () {
Route ::resource ('orders' , OrderController ::class );
});
Login flow is handled by Laravel Breeze or Fortify. Inertia requests include the session
cookie automatically.
Sanctum Token Authentication (For Mobile / External API)
public function login (Request $request ): JsonResponse
{
$request ->validate ([
'email' => 'required|email' ,
'password' => 'required' ,
'device_name' => 'required|string' ,
]);
$user = User ::where ('email' , $request ->email)->first ();
if (! $user || ! Hash ::check ($request ->password, $user ->password)) {
throw ValidationException ::withMessages ([
'email' => ['The provided credentials are incorrect.' ],
]);
}
$token = $user ->createToken ($request ->device_name)->plainTextToken;
return response ()->json ([
'token' => $token ,
'user' => new UserResource ($user ),
]);
}
public function logout (Request $request ): JsonResponse
{
$request ->user ()->currentAccessToken ()->delete ();
return response ()->json (null , 204 );
}
Client sends the token in every request:
Authorization: Bearer 1|abc123tokenvalue
Authentication Flow Summary Inertia (Monolith):
Browser -> POST /login (session cookie set) -> All subsequent requests carry cookie
API (Mobile/SPA):
Client -> POST /api/login (receives token) -> All subsequent requests carry Bearer token
Protecting Routes
Route ::middleware ('auth' )->group (function () { ... });
Route ::middleware ('auth:sanctum' )->group (function () { ... });
Route ::middleware (['auth:sanctum' , 'ability:orders:read' ])->group (function () {
Route ::get ('orders' , [OrderController ::class , 'index' ]);
});