ドキュメンテーション — 使いたくなるAPIドキュメント
ドキュメントなきAPIの悲劇
「使い方がわからないので、採用を見送ります」
新しいパートナー候補からのメール。テックブリッジのAPIは機能的には優れていたが、ドキュメントが貧弱だった。
「README.mdに箇条書きで書いてあるだけです」
サクラはPCの前で頭を抱えた。Stripeのドキュメントを見たとき、あの完成度に感動した記憶がある。コード例が動く、Try it出来る、エラーが何を意味するかが書いてある。
「ドキュメントもAPIの一部だ」
OpenAPI仕様
OpenAPI Specification(旧Swagger)は、APIを機械可読な形式で記述する業界標準だ。
# openapi.yaml
openapi: "3.1.0"
info:
title: TechBridge API
version: "2.0"
description: |
テックブリッジのパブリックAPI。
## 認証
すべてのAPIリクエストには `X-API-Key` ヘッダーが必要です。
## レート制限
プランによって異なります。詳細は[レート制限ページ](/docs/rate-limits)を参照してください。
contact:
email: api-support@techbridge.jp
url: https://docs.techbridge.jp
license:
name: MIT
servers:
- url: https://api.techbridge.jp/v2
description: 本番環境
- url: https://api-staging.techbridge.jp/v2
description: ステージング環境
security:
- ApiKeyAuth: []
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
schemas:
User:
type: object
required:
- id
- name
- email
properties:
id:
type: integer
example: 1
name:
type: string
minLength: 2
maxLength: 50
example: "田中太郎"
email:
type: string
format: email
example: "tanaka@example.com"
created_at:
type: string
format: date-time
example: "2024-01-15T09:00:00Z"
Error:
type: object
required:
- type
- status
- detail
properties:
type:
type: string
example: "https://docs.techbridge.jp/errors/not_found"
title:
type: string
example: "Not Found"
status:
type: integer
example: 404
detail:
type: string
example: "User (id: 999) が見つかりません"
request_id:
type: string
example: "req_abc123"
paths:
/users:
get:
summary: ユーザー一覧
description: |
ユーザーの一覧を返します。
### フィルタリング
`status` パラメーターでステータスをフィルタリングできます。
### ソート
`sort_by` と `order` パラメーターでソートできます。
operationId: listUsers
tags:
- Users
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
description: ページ番号
- name: per_page
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: 1ページあたりの件数
- name: status
in: query
schema:
type: string
enum: [active, inactive, suspended]
description: ステータスフィルター
responses:
"200":
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
meta:
type: object
properties:
pagination:
$ref: "#/components/schemas/Pagination"
example:
data:
- id: 1
name: "田中太郎"
email: "tanaka@example.com"
created_at: "2024-01-15T09:00:00Z"
meta:
pagination:
current_page: 1
per_page: 20
total_pages: 5
total_count: 100
"401":
description: 認証エラー
content:
application/problem+json:
schema:
$ref: "#/components/schemas/Error"rswagでドキュメントを自動生成
rswagはRSpecテストからOpenAPIドキュメントを生成するgemだ。テストとドキュメントが常に同期する。
# Gemfile
group :development, :test do
gem 'rswag-api'
gem 'rswag-ui'
gem 'rswag-specs'
end
# インストール
rails generate rswag:install
# config/routes.rb
mount Rswag::Ui::Engine => "/api-docs"
mount Rswag::Api::Engine => "/api-docs"rswagテストの書き方
# spec/requests/api/v2/users_spec.rb
require "swagger_helper"
RSpec.describe "Users API", type: :request do
path "/api/v2/users" do
get "ユーザー一覧を取得する" do
tags "Users"
produces "application/json"
security [ApiKeyAuth: []]
parameter name: :page, in: :query, type: :integer, default: 1,
description: "ページ番号"
parameter name: :per_page, in: :query, type: :integer, default: 20,
description: "1ページあたりの件数(最大100)"
parameter name: :status, in: :query, type: :string,
enum: %w[active inactive], description: "ステータスフィルター"
response "200", "成功" do
schema type: :object,
properties: {
data: {
type: :array,
items: { "$ref" => "#/components/schemas/User" }
},
meta: { "$ref" => "#/components/schemas/PaginationMeta" }
}
let(:"X-API-Key") { create(:api_key).key }
let(:page) { 1 }
let(:per_page) { 20 }
before { create_list(:user, 3) }
run_test! do |response|
data = JSON.parse(response.body)
expect(data["data"].length).to eq(3)
expect(data["meta"]["pagination"]["total_count"]).to eq(3)
end
end
response "401", "認証エラー" do
schema "$ref" => "#/components/schemas/Error"
let(:"X-API-Key") { "invalid_key" }
run_test! do |response|
body = JSON.parse(response.body)
expect(body["status"]).to eq(401)
end
end
end
post "ユーザーを作成する" do
tags "Users"
consumes "application/json"
produces "application/json"
security [ApiKeyAuth: []]
parameter name: :user, in: :body, schema: {
type: :object,
required: %w[name email],
properties: {
name: { type: :string, example: "田中太郎" },
email: { type: :string, format: :email, example: "tanaka@example.com" }
}
}
response "201", "作成成功" do
schema "$ref" => "#/components/schemas/UserResponse"
let(:"X-API-Key") { create(:api_key).key }
let(:user) { { name: "田中太郎", email: "tanaka@example.com" } }
run_test!
end
response "422", "バリデーションエラー" do
schema "$ref" => "#/components/schemas/ValidationError"
let(:"X-API-Key") { create(:api_key).key }
let(:user) { { name: "", email: "invalid" } }
run_test! do |response|
body = JSON.parse(response.body)
expect(body["errors"]).not_to be_empty
end
end
end
end
endドキュメント生成コマンド:
rails rswag:specs:swaggerize生成された swagger/v2/swagger.yaml から、/api-docs でSwagger UIが表示される。
swagger_helper.rb
# spec/swagger_helper.rb
require "rails_helper"
RSpec.configure do |config|
config.swagger_root = Rails.root.join("swagger").to_s
config.swagger_docs = {
"v2/swagger.yaml" => {
openapi: "3.1.0",
info: {
title: "TechBridge API",
version: "v2",
description: "テックブリッジ パブリックAPI"
},
paths: {},
servers: [
{ url: "https://api.techbridge.jp", description: "本番" }
],
components: {
securitySchemes: {
ApiKeyAuth: {
type: :apiKey,
in: :header,
name: "X-API-Key"
}
},
schemas: {
User: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
email: { type: :string, format: :email },
created_at: { type: :string, format: "date-time" }
}
},
Error: {
type: :object,
properties: {
type: { type: :string },
status: { type: :integer },
detail: { type: :string },
request_id: { type: :string }
}
},
ValidationError: {
type: :object,
properties: {
type: { type: :string },
status: { type: :integer },
errors: {
type: :array,
items: {
type: :object,
properties: {
field: { type: :string },
code: { type: :string },
message: { type: :string }
}
}
}
}
}
}
}
}
}
config.swagger_format = :yaml
endINFO
rswagの最大のメリットは「テストとドキュメントの同期」です。テストが通ればドキュメントも正確。ドキュメントだけ先に書いて実装と乖離する問題がなくなります。
Developer Portalの構築
Loading diagram...
# app/controllers/docs_controller.rb
class DocsController < ApplicationController
skip_before_action :authenticate!
def index
render "docs/index"
end
def getting_started
render "docs/getting_started"
end
def changelog
@changes = ApiChangelog.order(released_at: :desc).page(params[:page])
render "docs/changelog"
end
end
# config/routes.rb
get "/docs", to: "docs#index"
get "/docs/getting-started", to: "docs#getting_started"
get "/docs/changelog", to: "docs#changelog"コードサンプルの多言語対応
ドキュメントには複数言語のサンプルを載せる。
# users一覧取得のサンプル
# Ruby
require 'net/http'
require 'json'
uri = URI('https://api.techbridge.jp/v2/users')
uri.query = URI.encode_www_form(page: 1, per_page: 20)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['X-API-Key'] = 'your_api_key'
response = http.request(request)
users = JSON.parse(response.body)['data']# Python
import requests
response = requests.get(
'https://api.techbridge.jp/v2/users',
headers={'X-API-Key': 'your_api_key'},
params={'page': 1, 'per_page': 20}
)
users = response.json()['data']// JavaScript (fetch)
const response = await fetch('https://api.techbridge.jp/v2/users?page=1&per_page=20', {
headers: {
'X-API-Key': 'your_api_key'
}
});
const { data: users } = await response.json();変更履歴(Changelog)の管理
# db/migrate/xxxx_create_api_changelogs.rb
class CreateApiChangelogs < ActiveRecord::Migration[7.1]
def change
create_table :api_changelogs do |t|
t.string :version, null: false
t.date :released_at, null: false
t.string :change_type, null: false # added, changed, deprecated, removed, fixed
t.text :description, null: false
t.boolean :breaking_change, default: false
t.timestamps
end
end
end
# データ例
ApiChangelog.create!([
{
version: "2.1.0",
released_at: Date.new(2024, 3, 1),
change_type: "added",
description: "`GET /users` に `status` フィルターを追加",
breaking_change: false
},
{
version: "2.0.0",
released_at: Date.new(2024, 1, 1),
change_type: "changed",
description: "`username` フィールドを `name` に変更",
breaking_change: true
}
])AWS S3 + CloudFrontでのドキュメントホスティング
# CloudFormation
DocsS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: docs.techbridge.jp
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: 404.html
DocsCloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !Sub "${DocsS3Bucket}.s3-website-${AWS::Region}.amazonaws.com"
Id: DocsOrigin
CustomOriginConfig:
HTTPPort: 80
OriginProtocolPolicy: http-only
DefaultCacheBehavior:
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: !Ref DocsCachePolicy
TargetOriginId: DocsOrigin
Aliases:
- docs.techbridge.jp# ドキュメントのデプロイスクリプト
#!/bin/bash
rails rswag:specs:swaggerize # OpenAPIドキュメント生成
# ドキュメントサイトをビルド
cd docs-site && npm run build
# S3にデプロイ
aws s3 sync docs-site/dist s3://docs.techbridge.jp --delete
# CloudFrontキャッシュを無効化
aws cloudfront create-invalidation \
--distribution-id XXXXX \
--paths "/*"WARNING
ドキュメントは最高の営業ツールです。パートナー候補はドキュメントを見てAPIの品質を判断します。機能よりもドキュメントの完成度が採用の決め手になることさえあります。
サクラの達成感
数週間後、パートナー候補から返信が来た。
「ドキュメントが充実していて、Swagger UIで動作確認もできたので採用を決定しました」
「コードを書くのと同じくらい、ドキュメントを書くことが大切なんだ」
サクラはこれをチームのWikiに書いた:「ドキュメントはAPIの半分だ」。
次章では、APIの品質を保つテストとモニタリングを学ぶ。