10분 안에 자동 생성 OpenAPI 문서가 포함된 버전 관리 Laravel API 만들기
출처: Dev.to
TL;DR — dskripchenko/laravel-api 를 설치하고, 컨트롤러 하나만 작성하면 버전이 있는 API (/api/v1/...)와 그리고 /api/doc 에서 인터랙티브한 OpenAPI 3.0 문서를 자동으로 생성할 수 있습니다 — 문서 블록은 원래 작성하던 그대로 사용합니다. 그 뒤에 v2 를 복사‑붙여넣기 없이 바로 배포할 수 있습니다.
문제
성장하는 Laravel API에서는 두 가지가 점점 부패합니다:
버전 관리. v1 을 배포하고 나서 v2 에서는 세 개의 엔드포인트만 바꾸고 나머지 스무 개는 그대로 유지해야 합니다. V2 폴더를 복사‑붙여넣기 하면 버그 수정이 두 곳에 나뉘고, 아니면 컨트롤러에 if ($version === 2) 같은 분기문을 삽입해야 합니다.
문서. OpenAPI 스펙은 코드를 병합하는 순간부터 어긋나기 시작합니다. 어노테이션 라이브러리(#[OA\Get(...)], 거대한 YAML 파일) 를 사용하면 API 를 두 번 설명해야 합니다 — 코드에 한 번, 어트리뷰트에 한 번.
이 패키지의 핵심 아이디어: 컨트롤러 자체가 이미 자신을 설명한다는 것. 메서드 이름, 요청 필드, 응답 형태를 일반 PHPDoc 형태로 한 번만 작성하면, 패키지가 라우트와 문서를 모두 파생시킵니다. 버전 관리는 순수 PHP 상속으로 해결됩니다.
그럼 시작해봅시다.
우리가 만들 것
작은 tasks API:
POST /api/v1/task/list— 작업 목록 조회POST /api/v1/task/create— 작업 생성- 인터랙티브 문서는
GET /api/doc(버전별 원시 스펙은/api/doc/{version})
그리고 v2 에서는 v1 을 건드리지 않고 새로운 엔드포인트를 추가합니다.
전체 파일 수: 약 4개.
Step 0 — 설치
composer require dskripchenko/laravel-api
php artisan vendor:publish --tag=laravel-api-config
// config/laravel-api.php
return [
'prefix' => 'api', // → /api/...
'uri_pattern' => '{version}/{controller}/{action}',
'available_methods' => ['get', 'post', 'put', 'patch', 'delete'],
'openapi_path' => 'public/openapi',
'doc_middleware' => [], // 여기서 /api/doc 을 잠글 수 있음
];
Step 1 — 컨트롤러 작성
특별한 것은 없습니다 — 패키지의 ApiController 를 상속하면 success(), error(), validationError(), created(), noContent(), notFound() 와 같은 응답 헬퍼를 바로 사용할 수 있습니다. docblock 자체가 문서가 됩니다:
<?php
namespace App\Api\V1\Controllers;
use App\Api\ApiController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TaskController extends ApiController
{
/**
* List tasks
*
* @output array $tasks
* @output integer $tasks.id
* @output string $tasks.title
* @output string $tasks.status
*
* @return JsonResponse
*/
public function list(Request $request): JsonResponse
{
return $this->success([
['id' => 1, 'title' => 'Pack for Vietnam', 'status' => 'done'],
['id' => 2, 'title' => 'Write this article', 'status' => 'open'],
]);
}
/**
* Create a task
*
* @input string $title Task title
* @input string ?$status Initial status [open,done]
*
* @output integer $id New task id
* @output string $title Task title
*
* @return JsonResponse
*/
public function create(Request $request): JsonResponse
{
return $this->created([
'id' => 3,
'title' => $request->input('title'),
]);
}
}
알아두면 좋은 docblock 규칙
?$page→ 선택적 필드.string $status ... [open,done]→ 대괄호 안 목록이 스펙에서 enum 으로 변환됩니다.@input integer(int64) $id/@input string(email) $email→ 형식(format) 을 포함한 타입.@input file $avatar→ 파일 업로드;@input @User $user→$ref로 컴포넌트 스키마를 참조.
모든 응답은 일관된 래퍼에 감싸집니다:
{ "success": true, "payload": { ... } }
에러(ApiException 혹은 $this->error())는 다음 형태로 반환됩니다:
{ "success": false, "payload": { "errorKey": "string", "message": "string" } }
Step 2 — 버전, 모듈, 프로바이더 연결
세 개의 작은 클래스로 라우팅 레이어 전체를 구성합니다 — routes/api.php 에는 아무것도 추가하지 않습니다.
<?php
// app/Api/Modules/TaskModule.php
namespace App\Api\Modules;
class TaskModule
{
public function getRoutes(): array
{
return [
'task' => [ // → /api/v1/task/{action}
'controller' => \App\Api\V1\Controllers\TaskController::class,
'actions' => ['list', 'create'],
],
];
}
}
<?php
// app/Api/V1/Api.php
namespace App\Api\V1;
use App\Api\Api;
use App\Api\Modules\TaskModule;
class Api extends \App\Api\Api
{
protected function getModules(): array
{
return [
TaskModule::class,
];
}
}
<?php
// app/Api/Api.php
namespace App\Api;
abstract class Api
{
abstract protected function getModules(): array;
public function getRoutes(): array
{
$routes = [];
foreach ($this->getModules() as $module) {
$moduleInstance = new $module();
$routes = array_merge($routes, $moduleInstance->getRoutes());
}
return $routes;
}
}
// config/laravel-api.php (추가)
'modules' => [
\App\Api\V1\Api::class,
],
Step 3 — 서비스 프로바이더 등록
<?php
// app/Providers/ApiServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Api\Api;
class ApiServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('api', function () {
return new Api();
});
}
public function boot()
{
// 라우트 자동 등록
$this->app['router']->group([
'prefix' => config('laravel-api.prefix'),
'middleware' => config('laravel-api.doc_middleware'),
], function ($router) {
foreach (config('laravel-api.modules') as $apiClass) {
$api = new $apiClass();
foreach ($api->getRoutes() as $controllerKey => $info) {
$controller = $info['controller'];
foreach ($info['actions'] as $action => $method) {
$uri = sprintf('%s/%s/%s', $controllerKey, $action);
$router->match($method, $uri, [$controller, $action]);
}
}
}
});
}
}
config/app.php 의 providers 배열에 App\Providers\ApiServiceProvider::class 를 추가합니다.
Step 4 — 결과: 무료 OpenAPI 문서
브라우저에서 GET /api/doc 을 열어보세요. 원시 JSON 파일이 아니라 인터랙티브 API 레퍼런스( Scalar 로 렌더링) 가 표시됩니다. 버전 전환 스위처가 v1, v2, … 로 이미 연결돼 있습니다. 패키지는 컨트롤러를 스캔해 docblock을 읽고 완전한 OpenAPI 3.0 문서를 자동 생성합니다 — 파라미터, enum, 응답 스키마 등 모든 것이 포함됩니다. 이 문서는 코드와 동기화돼 있기 때문에 어긋날 수 없습니다.
CI, 클라이언트 생성기, 혹은 자체 Redoc/Stoplight 설정을 위해 원시 스펙이 필요하다면 각 버전은 GET /api/doc/{version} (예: /api/doc/v1) 에서 JSON 형태로 제공됩니다 — storage:link 나 별도 빌드 단계가 필요 없습니다.
TypeScript 클라이언트가 필요하나요? 다음 명령으로 생성할 수 있습니다:
php artisan api:generate-types