9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swagger UI × Docker で OpenAPI ドキュメントを自動化してみた

9
Last updated at Posted at 2025-12-11

はじめに

「Swagger は便利だから使おう」と言われても、

  • API って何なのか
  • OpenAPI / Swagger / Swagger UI の違い
  • Laravel にどう組み込むのか

最初はなかなかイメージしづらいものです。

私が実務で初めて Swagger(OpenAPI)を導入したときも、概念やツールの関係性、実装との紐づけ方などでつまずく場面が多くありました。

そこで本記事では、私がそのときに理解したポイントを整理しつつ、

  • 「API / OpenAPI / Swagger / Swagger UI」の関係
  • Laravel + Docker への Swagger UI の導入手順
  • サンプルの書籍管理 API を題材に、OpenAPI と実装を揃えていく方法

を分かりやすくまとめていきます。


そもそも API / OpenAPI / Swagger UI って何?

「APIって何?」ざっくりイメージ

20251210-6daf5b4a463e489684eda4dbda84efe7-attachment.jpg

API(Application Programming Interface) は、
ざっくり言うと「アプリ同士が会話するための決まりごと」です。

  • レストランでいうと → メニュー表
  • 「この URL に、こういうデータを送ると、こういう結果が返ってくるよ」という約束

フロントエンド(画面)や他のサービスは、この「メニュー表(API の仕様)」を見ながら

この API にこういうパラメータでリクエストすればいいんだな

と理解します。


OpenAPI って何?

20251210-5a9bb45fec3f414bb7e1ea83b35daa87-attachment.jpg

OpenAPI は、その「API の仕様」を

  • 人間にも
  • ツール(機械)にも

読みやすい形で記述するための フォーマット(ルール) です。

例えば:

  • GET /books はこういうレスポンスを返します」
  • POST /books のリクエストボディはこういう JSON です」
  • 「この項目は string で必須です」

といった情報を YAML / JSON で書き下せます。

OpenAPI 形式で書いておくと…

  • Swagger UI などで きれいな API ドキュメントを自動生成できる
  • OpenAPI Generator などで クライアントコードや型定義を自動生成できる
  • 契約テストなどで 仕様と実装のズレを検知しやすくなる

といったメリットがあります。


Swagger / Swagger UI って何?

20251210-b5f58e22acca4f178121dd60eba33df1-attachment.jpg

名前が似ていてややこしいので整理します。

  • Swagger(旧称)
    OpenAPI の前身となる仕様の名前。
    現在は仕様の正式名称は「OpenAPI」になり、Swagger はツール群のブランド名として残っています。

  • Swagger UI
    OpenAPI 仕様(YAML / JSON)を読み込んで、
    ブラウザ上に 「見やすい API ドキュメント + その場で試せる画面」 を生成してくれるツール。

ざっくりまとめると:

  • OpenAPI:API の仕様書(設計図)
  • Swagger UI:仕様書を読み込んで いい感じのカタログ画面にしてくれるビューア

という関係です。


OpenAPI を図でイメージしてみる

文字だけだとイメージしづらいので、関係性を図にしてみます。

20251210-57763dc23e2e4bac9908dad2e94356bc-attachment.jpg

  • Laravel の API サーバー
    実際にリクエストを受けてレスポンスを返す本体
  • OpenAPI 仕様書(openapi.yaml
    「どんな URL があって、どんなリクエスト・レスポンスなのか」を定義した設計図
  • Swagger UI
    その設計図を読み込んで、ブラウザ上にドキュメントとして表示+試せるようにしてくれるツール

つまり、

  • Laravel:実際に動く API
  • OpenAPI:API の仕様書
  • Swagger UI:仕様書(OpenAPI)を人間が見やすい形にしてくれるビューア

という役割分担になっています。


OpenAPI ファイル(openapi.yaml)の中身の構造も、ざっくり図にしておきます。

  • paths
    → 「どの URL に、どんなメソッド(GET / POST…)があるか」
  • components.schemas
    → 「Book や Error などの共通スキーマ」を定義する場所

この記事では、この中の pathscomponents.schemas を中心に、Laravel の実装と揃えていきます。


この記事で学べること

この記事を通じて、以下の流れを一通り体験できます。

  • Docker Compose で Swagger UI を導入する方法
  • OpenAPI 仕様書(YAML)の 基本構造と書き方
  • Laravel の API 実装と OpenAPI ドキュメントを 同じ形に揃えるコツ
  • 一覧・詳細・検索・ページネーション・エラーなど、
    実務でよく出てくる API パターンの定義方法

「まずは Swagger UI でいい感じのドキュメントが見えるようにしたい」
「後からちゃんとした設計に育てていきたい」

という人向けの内容です。


使用技術

技術 バージョン 用途
Laravel 11.x PHPフレームワーク
Docker / Docker Compose - コンテナ環境
Swagger UI latest APIドキュメント表示
OpenAPI 3.0.3 API仕様記述フォーマット

サンプルプロジェクト概要

例として、シンプルな書籍管理システムの API を題材にします。

実装する主な API は以下のとおりです。

  • 書籍API (/books)

    • 書籍一覧の取得(検索・ページネーション対応)
    • 書籍の詳細取得
    • 書籍の新規登録・更新・削除(CRUD)
  • 著者API (/authors)

    • 著者一覧・詳細の取得(読み取り専用)
  • カテゴリAPI (/categories)

    • カテゴリのマスターデータ(読み取り専用)

「1つの Book に Author と Category が紐づいている」よくある構成を想定しています。


環境構築

1. Docker ComposeにSwagger UIを追加

すでに Laravel + MySQL などのサービスがある前提で、
compose.yaml(または docker-compose.yml)に Swagger UI 用のサービスを 1 つ追加します。

services:
  # ... 既存のサービス(app, mysql等)

  swagger-ui:
    image: "swaggerapi/swagger-ui:latest"
    ports:
      - "${FORWARD_SWAGGER_PORT:-8080}:8080"
    environment:
      SWAGGER_JSON: /openapi/openapi.yaml
    volumes:
      - "./openapi.yaml:/openapi/openapi.yaml:ro"
    networks:
      - app-network

ここでやっていること

  • swaggerapi/swagger-ui:latest イメージを使って Swagger UI コンテナを起動
  • SWAGGER_JSON で読み込む OpenAPI ファイルを指定
    → コンテナ内では /openapi/openapi.yaml として扱う
  • ローカルの ./openapi.yaml をコンテナの /openapi/openapi.yaml にマウント(読み取り専用)

2. OpenAPI仕様書の雛形を作成

まだ中身はあとで埋めるので、まずは形だけ用意します。
プロジェクトルートに openapi.yaml を作成します。

openapi: 3.0.3
info:
  title: Book Management API
  version: 1.0.0
  description: 書籍管理システム API

servers:
  - url: http://localhost/api
    description: ローカル開発環境

tags:
  - name: books
    description: 書籍
  - name: authors
    description: 著者
  - name: categories
    description: カテゴリ

paths:
  # ここにエンドポイントを定義

components:
  schemas:
    # ここにスキーマを定義

この状態でも、Swagger UI は起動できます。


3. 起動確認

# Swagger UI コンテナを起動
docker compose up -d swagger-ui

# ブラウザでアクセス(Mac の場合)
open http://localhost:8080

ブラウザで Swagger UI が表示されれば成功です。

スクリーンショット 2025-12-11 14.56.02.png


OpenAPI仕様書の書き方(Book API 編)

ここからは、openapi.yaml の中身を実際の API に近づけていくステップです。
例として GET /books から順に追加していきます。

基本的なGETエンドポイント(一覧取得)

paths:
  /books:
    get:
      tags:
        - books
      summary: 書籍一覧を取得
      description: 登録されている全ての書籍を取得します
      operationId: getBooks
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Book'

この定義を追加して openapi.yaml を保存し、Swagger UI をリロードすると、
画面左側のエンドポイント一覧に GET /books が表示されます。
screencapture-localhost-8081-2025-12-11-14_58_45.png

書籍一覧API(GET /books)が Swagger UI 上に反映された状態。「Try it out」から実際にリクエストも送れる

「Try it out」ボタンから実際にリクエストを投げることもできるので、
API 実装後の動作確認やフロントエンドとのすり合わせにも便利です。


パスパラメータ付きエンドポイント(詳細取得)

  /books/{id}:
    get:
      tags:
        - books
      summary: 書籍詳細を取得
      operationId: getBookById
      parameters:
        - name: id
          in: path
          required: true
          description: 書籍ID
          schema:
            type: integer
            example: 1
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Book'
        '404':
          description: 書籍が見つかりません
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

/books/{id} のような URL の {id} 部分は、
parametersパスパラメータ として定義します。


クエリパラメータとページネーション

ページネーションや検索条件も OpenAPI に書いておくと、
フロント側や他チームとのコミュニケーションがかなり楽になります。

  /books:
    get:
      tags:
        - books
      summary: 書籍一覧を取得
      operationId: getBooks
      parameters:
        - name: page
          in: query
          description: ページ番号
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          description: 1ページあたりの件数
          schema:
            type: integer
            default: 15
        - name: category_id
          in: query
          description: カテゴリIDでフィルタ
          schema:
            type: integer
        - name: keyword
          in: query
          description: タイトル検索キーワード
          schema:
            type: string
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedBooks'

POSTエンドポイント(新規作成)

  /books:
    post:
      tags:
        - books
      summary: 書籍を登録
      operationId: createBook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookCreate'
      responses:
        '201':
          description: 作成成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Book'
        '422':
          description: バリデーションエラー
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
  • requestBody で入力データの形式を定義
  • バリデーションエラー 422 をあらかじめ定義しておくと、
    実装時・テスト時に「仕様と合っているか」を確認しやすくなります。

PUT/DELETEエンドポイント(更新・削除)

  /books/{id}:
    put:
      tags:
        - books
      summary: 書籍を更新
      operationId: updateBook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookUpdate'
      responses:
        '200':
          description: 更新成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Book'

    delete:
      tags:
        - books
      summary: 書籍を削除
      operationId: deleteBook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: 削除成功
        '404':
          description: 書籍が見つかりません

スキーマ定義(Book / Author / Categoryなど)

components:
  schemas:
    Book:
      type: object
      properties:
        id:
          type: integer
          example: 1
        title:
          type: string
          example: Laravel実践入門
        author:
          $ref: '#/components/schemas/Author'
        category:
          $ref: '#/components/schemas/Category'
        isbn:
          type: string
          example: "978-4-12345-678-9"
        published_at:
          type: string
          format: date
          example: "2024-01-15"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    BookCreate:
      type: object
      required:
        - title
        - author_id
        - category_id
      properties:
        title:
          type: string
          example: Laravel実践入門
        author_id:
          type: integer
          example: 1
        category_id:
          type: integer
          example: 1
        isbn:
          type: string
          example: "978-4-12345-678-9"
        published_at:
          type: string
          format: date

    Author:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: 山田太郎

    Category:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: プログラミング

    PaginatedBooks:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Book'
        meta:
          type: object
          properties:
            current_page:
              type: integer
            last_page:
              type: integer
            per_page:
              type: integer
            total:
              type: integer

    Error:
      type: object
      properties:
        message:
          type: string
          example: Resource not found

    ValidationError:
      type: object
      properties:
        message:
          type: string
          example: The given data was invalid.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

ここまで書くと、Swagger UI 上で「書籍 API 一式」がかなりそれっぽく表示されるようになります 🙌

全体画像

screencapture-localhost-8081-2025-12-11-15_03_36.png


LaravelでのAPI実装

ここからは、OpenAPI に書いた内容を Laravel の実装で再現していきます。

ディレクトリ構成

app/
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       ├── BookController.php
│   │       ├── AuthorController.php
│   │       └── CategoryController.php
│   └── Requests/
│       └── Api/
│           ├── StoreBookRequest.php
│           └── UpdateBookRequest.php
├── Services/
│   └── BookService.php
└── Models/
    ├── Book.php
    ├── Author.php
    └── Category.php

役割としてはざっくりこんな感じです。

  • Controller:HTTP リクエストの入口(どのメソッドを呼ぶか)
  • Request:バリデーション担当
  • Service:ビジネスロジックをまとめる場所
  • Model:DB とやり取りするクラス

コントローラーの実装例

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\StoreBookRequest;
use App\Http\Requests\Api\UpdateBookRequest;
use App\Services\BookService;
use Illuminate\Http\JsonResponse;

class BookController extends Controller
{
    public function __construct(
        private BookService $bookService
    ) {}

    public function index(): JsonResponse
    {
        $books = $this->bookService->getAll();

        return response()->json([
            'data' => $books->items(),
            'meta' => [
                'current_page' => $books->currentPage(),
                'last_page' => $books->lastPage(),
                'per_page' => $books->perPage(),
                'total' => $books->total(),
            ]
        ]);
    }

    public function show(int $id): JsonResponse
    {
        $book = $this->bookService->findById($id);

        if (!$book) {
            return response()->json([
                'message' => 'Resource not found'
            ], 404);
        }

        return response()->json([
            'data' => $book
        ]);
    }

    public function store(StoreBookRequest $request): JsonResponse
    {
        $book = $this->bookService->create($request->validated());

        return response()->json([
            'data' => $book
        ], 201);
    }

    public function update(UpdateBookRequest $request, int $id): JsonResponse
    {
        $book = $this->bookService->update($id, $request->validated());

        return response()->json([
            'data' => $book
        ]);
    }

    public function destroy(int $id): JsonResponse
    {
        $this->bookService->delete($id);

        return response()->json(null, 204);
    }
}

ルーティング設定

// routes/api.php
use App\Http\Controllers\Api\BookController;
use App\Http\Controllers\Api\AuthorController;
use App\Http\Controllers\Api\CategoryController;

Route::apiResource('books', BookController::class);
Route::apiResource('authors', AuthorController::class)->only(['index', 'show']);
Route::apiResource('categories', CategoryController::class)->only(['index', 'show']);

Route::apiResource を使うことで、OpenAPI で書いた CRUD と対応したルートが一気に定義できます。


テスト戦略

OpenAPI で「こういうレスポンス」と決めたら、テストでもその形を検証しておくと安心です。

Feature Test の実装例

<?php

namespace Tests\Feature\Api;

use App\Models\Book;
use App\Models\Author;
use App\Models\Category;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_get_books_list(): void
    {
        Book::factory()->count(3)->create();

        $response = $this->getJson('/api/books');

        $response->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    '*' => ['id', 'title', 'author', 'category', 'created_at']
                ],
                'meta' => ['current_page', 'last_page', 'per_page', 'total']
            ]);
    }

    public function test_can_get_book_by_id(): void
    {
        $book = Book::factory()->create();

        $response = $this->getJson("/api/books/{$book->id}");

        $response->assertStatus(200)
            ->assertJsonPath('data.id', $book->id);
    }

    public function test_can_create_book(): void
    {
        $author = Author::factory()->create();
        $category = Category::factory()->create();

        $response = $this->postJson('/api/books', [
            'title' => 'テスト書籍',
            'author_id' => $author->id,
            'category_id' => $category->id,
        ]);

        $response->assertStatus(201)
            ->assertJsonPath('data.title', 'テスト書籍');
    }

    public function test_returns_404_for_non_existent_book(): void
    {
        $response = $this->getJson('/api/books/999');

        $response->assertStatus(404)
            ->assertJsonPath('message', 'Resource not found');
    }

    public function test_returns_422_for_invalid_data(): void
    {
        $response = $this->postJson('/api/books', []);

        $response->assertStatus(422)
            ->assertJsonStructure(['message', 'errors']);
    }
}

段階的な実装の進め方

OpenAPI を活用した API 開発は、次のようなステップで進めるとスムーズです。

Phase 1: 基盤構築

  1. Docker Compose で Swagger UI を導入
  2. openapi.yaml の雛形を作成
  3. Swagger UI で表示・動作を確認

Phase 2: マスターデータAPI

  1. 読み取り専用のシンプルな API から実装
  2. カテゴリ・著者などの参照データを実装
# 読み取り専用APIの例
/categories:
  get:
    summary: カテゴリ一覧取得
/categories/{id}:
  get:
    summary: カテゴリ詳細取得

Phase 3: メインリソースAPI

  1. CRUD 操作を持つメイン API を実装
  2. リレーションを含めたレスポンス設計を行う

Phase 4: 高度な機能

  1. ページネーション対応
  2. 検索・フィルタリング機能
  3. ファイルアップロードなどの拡張機能

API設計のベストプラクティス

1. 一貫したレスポンス形式

// 単一リソース
{
  "data": { ... }
}

// コレクション(ページネーションあり)
{
  "data": [ ... ],
  "meta": {
    "current_page": 1,
    "last_page": 10,
    "per_page": 15,
    "total": 150
  }
}

// エラー
{
  "message": "The given data was invalid.",
  "errors": {
    "title": ["タイトルは必須です"]
  }
}
  • data に実データ
  • meta にページネーション情報
  • エラー時は messageerrors を統一

のように形式を揃えておくと、フロントエンド側も扱いやすくなります。


2. 適切な HTTP ステータスコード

コード 用途
200 成功(GET, PUT, PATCH)
201 作成成功(POST)
204 削除成功(DELETE)
400 不正なリクエスト
401 認証エラー
403 権限エラー
404 リソースが見つからない
422 バリデーションエラー
500 サーバーエラー

3. スキーマの再利用

同じ構造を複数箇所で使う場合は、$ref で参照して再利用します。

# 定義
components:
  schemas:
    Timestamps:
      type: object
      properties:
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

# 使用(allOfで合成)
Book:
  allOf:
    - $ref: '#/components/schemas/Timestamps'
    - type: object
      properties:
        id:
          type: integer
        title:
          type: string

こうしておくと、共通項目を追加・変更したいときにも 1 箇所直すだけで済みます。


トラブルシューティング

Swagger UI が表示されないとき

# コンテナのログを確認
docker compose logs swagger-ui

# ファイルのマウント状態を確認
docker compose exec swagger-ui ls -la /openapi/

# コンテナを再起動
docker compose restart swagger-ui
  • openapi.yaml のマウントパスが間違っていないか
  • ファイルが存在しているか

などを確認します。


YAML の構文エラー

# YAMLの構文チェック(Python)
python -c "import yaml; yaml.safe_load(open('openapi.yaml'))"

# swagger-cliでバリデーション
npx @apidevtools/swagger-cli validate openapi.yaml

よくある YAML エラー

# NG: インデントが揃っていない
properties:
id:
  type: integer

# OK: 正しいインデント
properties:
  id:
    type: integer

YAML はインデント命なので、スペースのズレでエラーになりやすい点に注意です。


まとめ

OpenAPI を活用した API 開発には、次のようなメリットがあります。

  1. ドキュメントの可視化
    Swagger UI で常に最新のドキュメントを確認できる
  2. フロントエンド連携の効率化
    型定義の自動生成など、フロント側とのすり合わせがスムーズになる
  3. API設計の標準化
    チーム全体で一貫した API 設計を共有できる
  4. テストの効率化
    Swagger UI の「Try it out」機能で手動テストもしやすい

参考リンク


お知らせ(採用情報)

最後にお知らせとなりますが、AppTime では一緒に働くメンバーを募集しております。
詳しくは採用情報ページをご確認ください。

AppTime 採用情報

みなさまからのご応募をお待ちしております。


最後までお読みいただきありがとうございました。
質問やフィードバックがあれば、ぜひコメントで教えてください

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?