api版本号应放在url路径中,如/api/v1/users,因其调试直观、支持cdn缓存、日志可读性强、文档生成简单;用accept头虽更restful但增加前端与中间件复杂度;推荐route::prefix()分组管理版本路由,控制器与验证逻辑按版本隔离,通用能力下沉至service或form request,错误响应格式须与版本严格对齐。

API 版本号该放在 URL 还是请求头里?
绝大多数 Laravel 项目选择 URL 路径分版本,比如 /api/v1/users。这不是因为“更标准”,而是因为调试直观、Nginx/CDN 可缓存、日志可读性强、Swagger 文档生成简单。用 Accept 请求头(如 Accept: application/vnd.app.v1+json)理论上更符合 REST,但实际中会增加前端调用复杂度、中间件判断开销、Laravel 路由匹配效率下降,且 IDE 和 Postman 都不友好。
用 Route::prefix() 分组是最直接的方案
在 routes/api.php 中按版本显式分组,不依赖第三方包,控制力最强:
Route::prefix('v1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
});
Route::prefix('v2')->group(function () {
Route::get('/users', [UserV2Controller::class, 'index']); // 行为变更
Route::post('/users', [UserV2Controller::class, 'store']); // 新增字段校验
});
- 每个版本对应独立控制器或方法,避免 if-else 判断版本分支
- 中间件(如
throttle:api)可按需绑定到某组,v2组可配更宽松限流 - 不要用
Route::version()—— Laravel 原生没有这个方法,是某些包伪造的语法糖,容易造成误解
如何让 v1 和 v2 共享部分逻辑又保持隔离?
共享不等于混写。推荐把通用能力下沉到 Service 或 Form Request,而非让控制器继承或复用路由:
- 验证规则差异大 → 各自定义
UserStoreRequestV1和UserStoreRequestV2,都继承FormRequest - 数据组装逻辑相似 → 提取
UserResponseBuilder类,v1/v2 控制器分别调用buildForV1($user)/buildForV2($user) - 数据库模型不变 → 模型层完全复用,版本差异只体现在 API 层(控制器 + 请求/响应)
硬塞一个“兼容模式”开关(如 if (app()->version === 'v2'))会让代码越来越难维护,尤其当 v3 上线后,条件分支会指数级膨胀。
别忽略中间件和异常响应的版本一致性
404、422、500 等错误返回格式必须和当前 API 版本对齐。例如 v2 要求错误结构为 {"error": {"code": "INVALID_EMAIL", "message": "..."} },就不能让 v2 请求落到 v1 的 App\Exceptions\Handler 默认 JSON 响应上。
- 在版本路由组内显式指定中间件:
Route::prefix('v2')->middleware(['api', 'version:v2'])->group(...) - 自定义中间件
EnsureApiVersion可拦截非法版本请求(如/api/v3/xxx),直接返回 400 - 重写
render()方法时,通过$request->route()?->parameter('version')或请求路径提取版本,动态选择错误模板
版本控制最易被跳过的环节,其实是错误响应格式和 HTTP 状态码语义——它们才是客户端真正依赖的契约。











