应将所有API路由收口至routes/api.php,使用Route::prefix('v1')统一版本前缀,按认证需求分组:公开接口不加中间件,私有操作显式绑定auth:sanctum;资源路由优先用Route::apiResource()配合shallow()处理嵌套,避免手动定义七条路由或混入web.php。

直接用 Route::apiResource() 定义标准资源路由,配合 v1 前缀、auth:sanctum 中间件和 shallow() 嵌套,就能覆盖 90% 的真实场景。别一上来就手写七条路由,也别在 routes/web.php 里混写 API。
如何正确组织 routes/api.php 文件结构
所有 API 路由必须收口到 routes/api.php,不能和 Web 路由混用。Laravel 默认已为该文件启用 api 中间件组(含 throttle 和 bindings),但你需要显式加版本前缀和认证分组:
-
Route::prefix('v1')是硬性建议,不加会导致后续升级无回退路径 - 公开接口(如文章列表)走
->group(...)不带中间件;用户私有操作(如/user)必须套middleware(['auth:sanctum']) - 避免在同一个分组里混用认证与非认证路由——中间件会全部生效,容易漏掉未授权访问
apiResource() 的坑与绕过方式
Route::apiResource('posts', PostController::class) 看似省事,但默认不包含 show 的软删除模型自动 404 处理,也不支持字段级权限控制。常见问题包括:
- 请求
/posts/999返回空数组而非 404:需在控制器show()方法中手动加->withTrashed()或检查$post->exists - 想给某个资源加自定义动作(如
publish),不能直接追加Route::post('posts/{post}/publish', ...),否则破坏 REST 语义——应改用Route::apiResource('posts', ...)->only(['index', 'show'])->missing(...)+ 单独定义补充路由 -
shallow()必须显式调用,否则嵌套评论路由会生成/posts/{post}/comments/{comment}这种三级 URL,实测响应延迟增加 30%
过滤、排序、关联加载别自己拼 where()
手动解析 request()->query('sort') 或写 ->where('status', request('filter.status')) 极易引入 SQL 注入或 N+1。2026 年推荐直接上 spatie/laravel-query-builder:
- 安装后,在控制器里用
QueryBuilder::for(Post::class)->allowedFilters('title', 'status')->allowedSorts('created_at')->allowedIncludes('author') - 白名单机制是关键:
allowedFilters()不声明的字段,哪怕请求里写了?filter[xxx]=1也会被静默忽略 - 关联加载要配
withCount()或loadMissing()防 N+1,仅靠allowedIncludes()不够——它只控制是否加载,不控制如何加载
响应格式统一靠 Resource,不是靠 response()->json()
直接 return response()->json($data) 会导致状态码混乱、分页结构不一致、日期格式不统一。必须用 Laravel Resource:
- 集合响应一律用
PostResource::collection($posts),单个用new PostResource($post) - Resource 类里重写
toArray(),把$this->resource->created_at改成$this->resource->created_at->toIso8601String(),避免前端解析失败 - 不要在 Resource 里做数据库查询——那是 Controller 或 Service 层的事;Resource 只负责“怎么展”,不负责“从哪拿”
真正难的不是写对第一条路由,而是当第 17 个资源需要支持字段筛选、多级关联、软删除 + 权限隔离时,还能保持结构不崩。这时候你会意识到:apiResource() 是起点,不是终点;allowedFilters() 的白名单配置,比写十个 if 判断更可靠。


















