绝大多数 Laravel 项目应将 API 版本号放在 URL 路径中(如 /api/v1/users),因其调试直观、缓存友好、日志可读、路由可控;Header 方式仅适用于严格语义化或客户端强依赖场景,且需避免混用、动态参数及跨版本耦合。

API版本号该放在URL里还是请求头里
绝大多数 Laravel 项目应该把版本号放在 URL 路径中,比如 /api/v1/users。不是因为“规范”,而是因为调试直观、缓存友好、日志可读、Nginx/CDN 路由可控——而把版本塞进 Accept 头(如 application/vnd.app.v1+json)会让 Postman 测试变麻烦,Laravel 的中间件和路由分组也难对齐。
常见错误现象:Route::middleware('accept.version:v1') 这类自定义中间件容易漏处理 OPTIONS 预检请求,或在 API 资源响应时忘记同步修改 Content-Type 头,导致前端收不到预期格式。
- 只在需要严格语义化或已有客户端强依赖 Accept 头时才选头方式
- URL 方式下,
v1、v2必须是静态段,别用{version}动态参数——否则无法用Route::prefix()精确隔离中间件和命名空间 - 不要混用:同一套 API 不要一部分走 URL 版本、一部分走 Header 版本,会增加网关层判断逻辑
Laravel 路由分组怎么绑定不同版本的控制器命名空间
靠 Route::prefix() + namespace() 组合最稳,而不是靠中间件动态切换命名空间——后者会让 IDE 跳转失效、PHPStan 静态分析报错、且 php artisan route:list 显示混乱。
示例:
Route::prefix('api')->group(function () {
Route::prefix('v1')->namespace('App\Http\Controllers\V1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
<pre class="brush:php;toolbar:false;">Route::prefix('v2')->namespace('App\Http\Controllers\V2')->group(function () {
Route::get('/users', [UserController::class, 'index']);
});});
- 每个版本对应独立命名空间,如
App\Http\Controllers\V1和App\Http\Controllers\V2,避免控制器方法名冲突 - 别在
V1\UserController里用use App\Http\Controllers\V2\SomeService——跨版本依赖会拖慢 v1 的迭代节奏 - 如果用了 Dingo API 或 Laravel Sanctum 的 token 验证,确保中间件注册顺序正确:版本路由组必须包裹认证中间件,而非反过来
如何让 v1 和 v2 共享部分逻辑又不耦合
共享逻辑不能靠继承(比如 V2\UserController extends V1\UserController),而应抽成策略类、服务类或 Form Request 验证器——控制器只负责协调,不承载业务规则。
常见错误现象:v2 接口改了字段校验,但忘了更新 v1 对应的 StoreUserRequest,结果 v1 的表单验证突然失败;或者 v2 加了新 Redis 缓存逻辑,v1 却意外被缓存 key 冲突影响。
- 验证逻辑统一用独立的
FormRequest类,按版本命名:如StoreUserRequestV1、StoreUserRequestV2 - 数据转换(DTO)、响应组装(Resource)必须按版本隔离,
UserResourceV1和UserResourceV2不共用同一个类 - 数据库模型保持稳定,版本差异尽量在 Service 层做适配,比如
UserProfileService::forVersion('v2')返回不同字段组合
升级到 v2 后 v1 接口还能不能热修复
能,但前提是 v1 的路由、控制器、验证器、资源全部冻结在 Git 分支或目录里,且生产环境部署时明确指定版本路径。一旦开始维护 v2,就不要再往 App\Http\Controllers\V1 里提交新 commit。
最容易被忽略的一点:数据库迁移不能只面向 v2 设计。比如 v2 新增了 is_premium 字段,v1 的用户列表接口虽然不返回它,但 ORM 查询若没显式指定 select(),可能因 Eloquent 默认查 * 而多带一个字段——多数情况无害,但在某些 MySQL 严格模式或 JSON 响应序列化时会出错。
- 所有 v1 的查询必须锁定字段,例如
User::select('id', 'name', 'email')->get() - v1 的数据库迁移文件要打上
v1-前缀并归档,v2 的迁移绝不能修改 v1 已发布的字段类型或约束 - 日志里记录请求的
route:name时,确保包含版本前缀,比如api.v1.users.index,方便监控区分流量分布



















