API版本必须放在URL路径中,如/api/v1/users;Accept头、查询参数或子域名方案会导致调试难、缓存错、日志乱、IDE跳转失效等问题,Route::prefix() + namespace()是唯一可控组合。

API 版本必须放在 URL 路径里,比如 /api/v1/users。Accept 头、查询参数或子域名方案在 Laravel 项目中会立刻暴露调试难、缓存错、日志乱、IDE 跳转失效等问题——这不是风格偏好,是工具链和运维事实决定的。
为什么不能用 Accept 头做版本路由
Accept 头(如 application/vnd.app.v2+json)在理论 REST 中成立,但落地时三处硬伤无法绕过:
- Postman、curl、Swagger UI 默认不带该头,漏一次就落到 v1 或直接 500
- Nginx/Cloudflare 可能 strip 或 normalize
Accept,导致路由匹配失效,且问题难以复现 -
php artisan route:list完全不显示版本信息,IDE 跳转、PHPStan 分析、Git blame 全部失焦
更关键的是:route:cache 不缓存基于 header 的匹配逻辑,v2 请求实际走的是未缓存的慢路径。
Route::prefix() + namespace() 是唯一可控组合
靠中间件动态切换命名空间(比如 app()->bind('UserController', ...))看似灵活,实则破坏静态分析。正确做法是路由层物理隔离:
- 每个版本用字面量前缀,禁止写成
{version}—— 否则中间件绑定和命名空间失效 - 子路由文件(如
routes/api/v1.php)里不要再套Route::prefix('v1'),否则变成/api/v1/v1/ - 控制器类名可以相同,但必须显式写全路径:
\App\Http\Controllers\V1\UserController::class - 资源路由必须显式指定 as 名称:
->as('api.v1.users'),否则route()辅助函数生成 URL 失败
控制器要不要新建?看语义是否断裂
不是“有改动就要新建”,而是“行为契约变了才必须隔离”:
- v2 只新增字段、微调分页参数 → 复用
V1\UserController,方法内轻量适配(如if ($request->version === 'v2') { ... }) - v2 改认证方式、重定义资源关系、变更幂等性规则 → 必须新建
V2\UserController,否则 git log 和权限审计无法追溯 - 绝对禁止
V2\UserController extends V1\UserController—— 继承会把 v1 的 bug、临时 patch、废弃逻辑一并带入
共享逻辑应抽成策略类或服务类,控制器只协调,不承载业务规则。
中间件和响应格式必须按版本独立控制
别指望一个全局 ApiResponsetrait 或中间件统一处理所有版本的返回结构:
- v1 返回
{data: {}},v2 改成{result: {}, meta: {}},硬统一只会让旧客户端崩溃 - 限流中间件也得拆开:
throttle:60,1和throttle:100,1分别绑定到 v1/v2 路由组 - Form Request 验证器必须分版本:
StoreUserRequestV1和StoreUserRequestV2,不要在单个rules()方法里 if-else 判版本
最容易被忽略的是:v1 和 v2 的资源类(UserResource)也必须完全隔离,避免在 toArray() 里读 request()->routeIs('api.v2.*') —— 路由信息在资源类里不可靠,且破坏可测试性。


















