用路由前缀(如v1、v2)分组定义API版本最稳妥,需配合对应命名空间控制器、独立验证规则和资源类,避免中间件动态判别或共享模型导致的兼容问题。

用路由前缀区分 API 版本最稳妥
直接在 routes/api.php 里按版本分组,是 Laravel 官方推荐且最不易出错的方式。它不依赖中间件拦截或请求头解析,避免了版本识别时机晚、调试困难的问题。
常见错误现象:有人试图用 Accept 请求头(如 application/vnd.app.v1+json)做路由分发,结果发现 Route::middleware('api') 已经跑完,中间件里改路由逻辑基本不可控,还容易和 CORS、认证顺序冲突。
- 版本前缀统一用
v1、v2这类语义化字符串,别用1.0或1.0.1—— 路由不支持点号通配,且后续升版时路径会变味 - 每个版本路由单独写文件更清晰,比如
routes/api/v1.php和routes/api/v2.php,再在主文件中require进来 - 不要把所有版本塞进同一个
Route::prefix('api')->group(...),否则 v1 和 v2 的中间件、命名空间、资源绑定全混在一起,后期删旧版时极易漏改
控制器命名空间必须跟路由前缀对齐
路由前缀是 v1,控制器就必须放在 App\Http\Controllers\V1\ 下,否则 php artisan route:list 看起来正常,但实际访问时抛 Class App\Http\Controllers\UserController does not exist —— 因为 Laravel 默认从根命名空间找,没自动补版本段。
使用场景:v2 新增字段或调整响应结构,但不想动 v1 的任何代码,这时靠命名空间隔离最干净。
- 生成控制器时明确指定命名空间:
php artisan make:controller V1/UserController - 在
routes/api/v1.php中用完整命名空间引用:[\App\Http\Controllers\V1\UserController::class, 'index'],别依赖隐式控制器绑定 - 如果用了资源路由,注意
Route::resource('users', 'UserController')默认不会自动加命名空间,得显式写成Route::resource('users', \App\Http\Controllers\V1\UserController::class)
响应格式和状态码要在各版本控制器里各自控制
别指望一个全局的 ApiResponse trait 或中间件统一处理所有版本的返回结构。v1 可能返回 {data: {...}},v2 改成 {result: {...}, meta: {...}},硬统一只会让旧客户端崩溃,或逼你在 v2 里伪造 v1 结构。
性能影响:中间件里做版本判断 + 动态切换响应包装器,每次请求多一次字符串匹配和条件分支,不如在控制器方法里直接写死——反正版本是静态路由决定的,没必要运行时猜。
- v1 控制器里用
return response()->json(['data' => $user]) - v2 控制器里用
return response()->json(['result' => $user, 'meta' => ['version' => 'v2']]) - 如果共用模型,注意 v2 的 Eloquent accessor 或
toArray()不要意外污染 v1 的输出,建议给不同版本建各自的 Resource 类(V1\UserResource/V2\UserResource)
数据库迁移和模型兼容性比路由更难收口
路由和控制器可以分版本,但 User 模型只有一个。v2 加了个 nickname 字段,v1 的客户端却没传,结果创建失败——这不是 API 层能兜住的,得靠模型层防御。
容易踩的坑:有人在 v2 路由里直接改 User::create($request->all()),忘了 v1 的请求数据结构不同,字段白名单没隔离,导致脏数据入库。
- 每个版本控制器里,用
$request->validate()显式声明该版本接受的字段,别复用同一份Request类 - 模型的
$fillable别放开全部字段,v1 和 v2 共用模型时,用$request->only([...])严格过滤 - 新增非空字段时,别直接加
NOT NULL迁移,先加可空字段,等 v1 流量归零后再补默认值和约束
版本控制真正的麻烦不在怎么标 v1/v2,而在旧版本字段删不得、改不得、默认值动不得——这些限制往往到线上出问题才被看见。


















