Laravel 13 JSON:API Resources 完全指南 摘要 Laravel 13 引入了第一方 JSON:API Resources,简化符合 JSON:API 规范的 API 构建。本文将全面解析 JSON:API Resources 的使用,包括:
JSON:API 规范核心概念 Laravel 13 JSON:API Resources 基础用法 资源对象序列化 关系包含与链接 稀疏字段集 分页与过滤 错误处理 本文适合希望构建标准化 API 的 Laravel 开发者。
1. JSON:API 规范概述 1.1 什么是 JSON:API JSON:API 是一种用于构建 API 的规范,定义了客户端如何请求和修改资源,以及服务器如何响应这些请求。主要特点:
统一的资源对象格式 标准化的关系处理 内置分页支持 错误响应规范 内容协商 1.2 基本响应格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "jsonapi" : { "version" : "1.1" } , "data" : { "type" : "articles" , "id" : "1" , "attributes" : { "title" : "JSON:API Example" , "content" : "This is an example." } , "relationships" : { "author" : { "data" : { "type" : "users" , "id" : "9" } } } , "links" : { "self" : "/api/articles/1" } } }
2. 基础用法 2.1 创建 JSON:API Resource 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace App \Http \Resources ;use Illuminate \Http \Resources \Json \JsonApiResource ;class ArticleResource extends JsonApiResource { public function toArray ($request ): array { return [ 'title' => $this ->title, 'content' => $this ->content, 'published_at' => $this ->published_at, ]; } }
2.2 控制器使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php namespace App \Http \Controllers \Api ;use App \Models \Article ;use App \Http \Resources \ArticleResource ;class ArticleController extends Controller { public function show (Article $article ) { return new ArticleResource ($article ); } public function index ( ) { return ArticleResource ::collection (Article ::all ()); } }
2.3 响应示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "jsonapi" : { "version" : "1.1" } , "data" : { "type" : "articles" , "id" : "1" , "attributes" : { "title" : "My Article" , "content" : "Article content..." , "published_at" : "2026-03-20T10:00:00Z" } , "links" : { "self" : "http://example.com/api/articles/1" } } }
3. 资源类型与 ID 3.1 自定义类型 1 2 3 4 5 6 7 class ArticleResource extends JsonApiResource { protected function resourceType ( ): string { return 'posts' ; } }
3.2 自定义 ID 1 2 3 4 5 6 7 class ArticleResource extends JsonApiResource { protected function resourceId ( ): string { return $this ->slug; } }
4. 关系处理 4.1 定义关系 1 2 3 4 5 6 7 8 9 10 11 class ArticleResource extends JsonApiResource { public function relationships ( ): array { return [ 'author' => $this ->relation (UserResource ::class , 'author' ), 'comments' => $this ->relation (CommentResource ::class , 'comments' ), 'tags' => $this ->relation (TagResource ::class , 'tags' ), ]; } }
4.2 包含关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "data" : { "type" : "articles" , "id" : "1" , "attributes" : {...}, "relationships" : { "author" : { "data" : {"type" : "users" , "id" : "9" } }, "comments" : { "data" : [ {"type" : "comments" , "id" : "1" }, {"type" : "comments" , "id" : "2" } ] } } }, "included" : [ { "type" : "users" , "id" : "9" , "attributes" : {"name" : "John Doe" } }, { "type" : "comments" , "id" : "1" , "attributes" : {"body" : "Great article!" } } ] }
4.3 关系链接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class ArticleResource extends JsonApiResource { public function relationships ( ): array { return [ 'author' => $this ->relation (UserResource ::class , 'author' ) ->withLinks () ->withData (), 'comments' => $this ->relation (CommentResource ::class , 'comments' ) ->withLinks () ->withoutData (), ]; } }
5. 稀疏字段集 5.1 基本用法 5.2 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ArticleResource extends JsonApiResource { public function toArray ($request ): array { return [ 'title' => $this ->title, 'content' => $this ->content, 'summary' => $this ->summary, 'published_at' => $this ->published_at, ]; } protected function sparseFieldsetsEnabled ( ): bool { return true ; } }
6. 分页 6.1 基本分页 1 2 3 4 5 6 7 8 9 class ArticleController extends Controller { public function index ( ) { $articles = Article ::paginate (15 ); return ArticleResource ::collection ($articles ); } }
6.2 分页响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "jsonapi" : { "version" : "1.1" } , "data" : [ ...] , "links" : { "first" : "/api/articles?page=1" , "last" : "/api/articles?page=10" , "prev" : "/api/articles?page=1" , "next" : "/api/articles?page=3" , "self" : "/api/articles?page=2" } , "meta" : { "current_page" : 2 , "from" : 16 , "last_page" : 10 , "per_page" : 15 , "to" : 30 , "total" : 150 } }
6.3 游标分页 1 2 3 4 5 6 public function index ( ) { $articles = Article ::cursorPaginate (15 ); return ArticleResource ::collection ($articles ); }
7. 过滤与排序 7.1 过滤 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ArticleController extends Controller { public function index (Request $request ) { $query = Article ::query (); if ($request ->has ('filter.status' )) { $query ->where ('status' , $request ->input ('filter.status' )); } if ($request ->has ('filter.author_id' )) { $query ->where ('author_id' , $request ->input ('filter.author_id' )); } return ArticleResource ::collection ($query ->paginate ()); } }
7.2 排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function index (Request $request ) { $sort = $request ->input ('sort' , '-created_at' ); $query = Article ::query (); if (str_starts_with ($sort , '-' )) { $query ->orderBy (substr ($sort , 1 ), 'desc' ); } else { $query ->orderBy ($sort , 'asc' ); } return ArticleResource ::collection ($query ->paginate ()); }
8. 错误处理 8.1 错误响应 1 2 3 4 5 6 7 8 use Illuminate \Http \Resources \Json \JsonApiError ;return JsonApiError ::make () ->status (404 ) ->code ('RESOURCE_NOT_FOUND' ) ->title ('Resource Not Found' ) ->detail ('The requested article does not exist.' ) ->source ('/data/id' );
8.2 错误响应格式 1 2 3 4 5 6 7 8 9 10 11 12 { "jsonapi" : { "version" : "1.1" } , "errors" : [ { "status" : "404" , "code" : "RESOURCE_NOT_FOUND" , "title" : "Resource Not Found" , "detail" : "The requested article does not exist." , "source" : { "pointer" : "/data/id" } } ] }
8.3 验证错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 use Illuminate \Http \Resources \Json \JsonApiError ;public function store (Request $request ) { $validator = Validator ::make ($request ->all (), [ 'title' => 'required|max:255' , 'content' => 'required' , ]); if ($validator ->fails ()) { $errors = JsonApiError ::collection (); foreach ($validator ->errors ()->messages () as $field => $messages ) { foreach ($messages as $message ) { $errors ->add ( JsonApiError ::make () ->status (422 ) ->title ('Validation Error' ) ->detail ($message ) ->source ("/data/attributes/{$field} " ) ); } } return $errors ->response (422 ); } }
9. 完整示例 9.1 资源类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php namespace App \Http \Resources ;use Illuminate \Http \Resources \Json \JsonApiResource ;class ArticleResource extends JsonApiResource { public function toArray ($request ): array { return [ 'title' => $this ->title, 'slug' => $this ->slug, 'content' => $this ->content, 'summary' => $this ->summary, 'status' => $this ->status, 'published_at' => $this ->published_at, 'created_at' => $this ->created_at, 'updated_at' => $this ->updated_at, ]; } public function relationships ( ): array { return [ 'author' => $this ->relation (UserResource ::class , 'author' ) ->withLinks () ->withData (), 'comments' => $this ->relation (CommentResource ::class , 'comments' ) ->withLinks (), 'tags' => $this ->relation (TagResource ::class , 'tags' ) ->withLinks (), ]; } public function links ( ): array { return [ 'self' => route ('api.articles.show' , $this ->id), ]; } }
9.2 控制器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 <?php namespace App \Http \Controllers \Api ;use App \Models \Article ;use App \Http \Resources \ArticleResource ;use Illuminate \Http \Request ;use Illuminate \Http \Resources \Json \JsonApiError ;class ArticleController extends Controller { public function index (Request $request ) { $query = Article ::query ()->with (['author' , 'tags' ]); if ($status = $request ->input ('filter.status' )) { $query ->where ('status' , $status ); } $sort = $request ->input ('sort' , '-created_at' ); $direction = str_starts_with ($sort , '-' ) ? 'desc' : 'asc' ; $field = ltrim ($sort , '-' ); $query ->orderBy ($field , $direction ); $include = $request ->input ('include' , '' ); return ArticleResource ::collection ($query ->paginate ()) ->withIncludes ($include ); } public function show (Article $article , Request $request ) { $article ->load (['author' , 'tags' ]); return (new ArticleResource ($article )) ->withIncludes ($request ->input ('include' , '' )); } public function store (Request $request ) { $validated = $request ->validate ([ 'data.attributes.title' => 'required|max:255' , 'data.attributes.content' => 'required' , 'data.attributes.status' => 'required|in:draft,published' , ]); $article = Article ::create ([ 'title' => $validated ['data' ]['attributes' ]['title' ], 'content' => $validated ['data' ]['attributes' ]['content' ], 'status' => $validated ['data' ]['attributes' ]['status' ], 'author_id' => auth ()->id (), ]); return (new ArticleResource ($article )) ->response (201 ) ->header ('Location' , route ('api.articles.show' , $article ->id)); } public function update (Article $article , Request $request ) { $validated = $request ->validate ([ 'data.attributes.title' => 'sometimes|max:255' , 'data.attributes.content' => 'sometimes' , 'data.attributes.status' => 'sometimes|in:draft,published' , ]); $article ->update ($validated ['data' ]['attributes' ] ?? []); return new ArticleResource ($article ); } public function destroy (Article $article ) { $article ->delete (); return response ()->noContent (); } }
9.3 路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 use Illuminate \Routing \Attributes \Get ;use Illuminate \Routing \Attributes \Post ;use Illuminate \Routing \Attributes \Put ;use Illuminate \Routing \Attributes \Delete ;use Illuminate \Routing \Attributes \Prefix ;#[Prefix ('api/v1' )] class ArticleRoutes { #[Get ('/articles' , name : 'api.articles.index' )] public function index ( ) {} #[Get ('/articles/{article}' , name : 'api.articles.show' )] public function show ( ) {} #[Post ('/articles' , name : 'api.articles.store' )] public function store ( ) {} #[Put ('/articles/{article}' , name : 'api.articles.update' )] public function update ( ) {} #[Delete ('/articles/{article}' , name : 'api.articles.destroy' )] public function destroy ( ) {} }
10. 最佳实践 10.1 资源命名 1 2 3 4 5 6 7 'type' => 'articles' 'type' => 'users' 'type' => 'article' 'type' => 'user'
10.2 版本控制 1 2 3 4 5 #[Prefix ('api/v1' )] class ArticleRoutes {}#[Prefix ('api/v2' )] class ArticleRoutesV2 {}
10.3 缓存策略 1 2 3 4 5 6 7 8 public function show (Article $article ) { return Cache ::remember ( "article.{$article->id} .jsonapi" , now ()->addHours (1 ), fn() => new ArticleResource ($article ) ); }
11. 总结 Laravel 13 的 JSON:API Resources 为构建标准化 API 提供了强大支持:
开箱即用 :无需额外包即可使用规范兼容 :完全符合 JSON:API 规范关系处理 :简化复杂关系的序列化分页支持 :内置分页链接生成错误处理 :标准化的错误响应通过本指南,您已经掌握了 JSON:API Resources 的核心用法,可以开始构建标准化 API 了。
参考资料