1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これだけは考慮して!Web API設計のベストプラクティス:『Web API: The Good Parts』から学ぶ実践的ガイド

Posted at

はじめに

Web API: The Good Parts
上記の本をやっと読むことができました。大変参考になり、これは必ず身につけなければならないと思い、自分が設計するならば、という立場での設計指針や考慮事項をまとめます。
また、一部古い内容もあり、調べながら執筆時点のベストプラクティスを可能な限り記載しました。
自分のメモ用にもなりますが、他の人にも見ていただき参考になれば幸いです。
ただし、内容は私個人でまとめたり調べてものになりますので、間違っている記載や良くない表現もあると考えております。
ご意見やご指摘があれば是非コメントお願いします。

対象

  • Web開発をされている方(主にバックエンド担当者だが、フロントエンド担当者も認識するべき)
  • Web APIを利用した事業に関わるプロジェクトリーダー・テックリード

API基本設計

エンドポイントの基本設計

短くて入力しやすいURI

NG
http://api.example.com/service/api/search
OK
http://api.example.com/search

人間が読んで理解できるURI

NG
http://api.example.com/sv/u
OK
http://api.example.com/service/users

大文字小文字が混雑していないURI

NG
http://api.example.com/Users/12345
http://example.com/API/GetUserName
OK
http://api.example.com/users/12345
http://example.com/api/users

複数形にすべきかどうかなどは後ほどのセクションで記載

改造しやすい(Hackable)URI

IDが1-300000の場合alphaエンドポイント、300001-500000の場合betaエンドポイントへアクセスするという場合

NG
http://api.example.com/v1/items/alpha/:id
http://api.example.com/v1/items/beta/:id
OK
http://api.example.com/v1/items/:id

サーバ側のアーキテクチャが反映されていないURI

NG
http://api.example.com/cgi-bin/get_user.php?user=100
OK
http://api.example.com/users?user=100

ルールが統一されたURI

NG
(友達の情報取得)http://api.example.com/friends?id=100
(メッセージの投稿)http://api.example.com/friends/100/message
OK
(友達の情報取得)http://api.example.com/friends/100
(メッセージの投稿)http://api.example.com/friends/100/messages

メソッドの使い分け

メソッド名 説明
GET リソースの取得
POST リソースの新規登録
PUT 既存リソースの更新
DELETE リソースの削除
PATCH リソースの一部変更
HEAD リソースのメタ情報の取得

※GETメソッドでは絶対にサーバ側の情報を変更させないようにする。
 クローラからのアクセスがあり、意図しない変更が入ったり、同一生成元ポリシーによるセキュリティ向上の恩恵が受けられない可能性があるため。

エンドポイントの設計の注意点

複数形の名詞を利用する

users、friends、updatesといった「リソースの集合」を表す複数形を利用する。
(DBでもテーブル名が複数形を用いることが適切であるため、この内容に沿うようにする)
名詞である理由は、HTTPのメソッドが動詞を表すためであり、短くて入力しやすいURIの原則に沿うようにするために、動詞は極力エンドポイントに入れないことが基本である。

利用する単語を気をつける

例えば「何かを探す」という場合、searchとfindが候補に上がるが、前者は探す場所を目的語をとり、後者は探すものを目的語に取る。seaechの方が広範囲から特定の条件に合致する複数のものを探すニュアンスがあり、その他のAPIでもsearchがよく使われている。他の例では写真ではpictureではなく、photoの方が使われる。どの単語を使うかは他のAPIを参考にする。

スペースやエンコードを必要とする文字を使わない

URIに利用できない文字はパーセントエンコーディングと呼ばれる表記方法が使われてしまい、可読性が下がってしまうため利用してはいけない。

NG
http://api.example.com/v1/%E3%83%A6%E3%83%BC%E3%82%B6/123
OK
http://api.example.com/v1/users/123

単語を繋げる必要がある場合はハイフンを利用する

公開されているAPIを見る限り、かなりバラバラな状態であるが、ホスト名のルールに沿ってスパイナルケース(ケバブケース)を利用して表現すべきである。

NG
http://api.example.com/v1/users/12345/profile_image
http://api.example.com/v1/users/12345/profileImage
OK
http://api.example.com/v1/users/12345/profile-image

(補足にはなるが、ホスト名はRFC952、1123で規定されており、アンダースコアは許可されていない。)

検索とクエリパラメータの設計

ページネーションの仕組みを検討する

一覧を取得するようなAPIに対して、データが1万以上あり、全てを一度に取得する場合、データのサイズが大きすぎて問題になる。
そこでページネーションと呼ばれる仕組み、SQLではlimitとoffsetのような値を指定して取得できるように実装する。

ページネーション例
per_page=50&page=3
limit=50&offset=100

pageやoffsetといった相対的な取得位置でデータを取得すると以下の問題点がある。

  • パフォーマンス
    • データベース上では相対的な数値を使った位置の指定は遅くなるケースがあるため
  • データの不整合
    • 更新頻度が高いデータの場合、最初の取得から次の取得の間でデータが更新されてしまい、実際に取得したい位置と取得された位置がズレてしまうことがある

そのため絶対位置指定でのデータ取得を検討する。絶対位置指定とは、最後に取得したデータのIDや時刻を記録してその値よりも前のもの、古いものを指定することである。
この方法を用いることで、上記の問題を解決できる。

絞り込みのためのクエリパラメータとパスの使い分け

以下二つの観点でクエリパラメータに含めるか、パスに入れるかを判断する

  1. 一意のリソースを表すのに必要な情報かどうか
  2. 省略可能かどうか

1に関して、一意に特定できる情報であれば、参照したい情報が明確なのでパスに入れた方が良い。
2に関して、省略可能であれば、デフォルト値が勝手に適応されるなどのユースケースが多いので、クエリパラメータを利用した方が良い。

パスパラメータ例
http://api.example.com/v1/users/12345
クエリパラメータ例
http://api.example.com/v1/users?since_id=2025

認証・認可について

当然のことではあるが、APIはトークンなどを用いて、認証、認可できる形で提供する。
ここではOAuth2.0などの記載は省略する。

レスポンスデータの設計

データフォーマットとその指定

データフォーマットは基本的にJSONに対応していれば良い。ドメインや外部連携サービスによって、XMLのデータフォーマットのサポートを検討する。
もし複数フォーマットに対応する場合、リクエスト側でデータフォーマットを指定する方法は以下の3種類が考えられる。

  1. クエリパラメータを使う方法
http://api.example.com/v1/users?format=xml
  1. 拡張子を使う方法
http://api.example.com/v1/users.json
  1. リクエストヘッダでメディアタイプを指定する方法
GET /v1/users
Host: api.example.com
Accept: application/json

3が最も行儀の良い方法ではあるが、1の方法でサポートされているものが多い。
基本的には、リクエストヘッダーでの対応を検討する。

内部構造の設計

  • APIのアクセス回数をなるべく減らすようにする
    • 例えばSNSの友人取得APIでIDのみ返す場合、クライアントはそのIDを用いてユーザ情報取得を行う必要があり、最低2回のアクセスが必要になってしまい、パフォーマンスに影響が出てしまうため
    • APIのユースケースを考えて返すデータを考える
  • レスポンスの内容をユーザが選べるようにする
    • APIへのアクセス回数は最小限に考えるべきだが、データ量が多すぎた場合、ダウンロードにも処理にも時間がかかってしまうため
    • 例えば以下のように利用するデータをクエリパラメータを使って制御できるようにする
field1による取得パラメータ制御例
http://api.example.com/v1/users/12345?field=name,age

オーバーフェッチやアンダーフェッチはREST APIで問題視されていた。
その解決のために、GraphQLという方法がある。
ある程度、設計が固まっている場合は問題ないが、制御するパラメータが多かったり可変的に変わっていくケースが考えられる場合などは、GraphQLの採用を検討する。
ただし、学習コストが高く歴史も浅いため、開発チームの成熟度も考慮する。

  • エンベロープは可能な限り避ける
    • HTTPの仕組みにヘッダの概念があり、メタ情報はレスポンスヘッダに記載すべきであるため
    • ただし、なんでもフラットなデータ構造にすべきではなく、例えばどちらも同じユーザという構造を表す場合は、階層化し可読性を向上させることも検討する
階層的に表す場合
{
    "id": 12345,
    "message": "Hello",
    "sender": {
        "id": 3456,
        "name": "Taro Yamada"
    },
    "receiver": {
        "id": 6789,
        "name": "Kenji Suzuki"
    },
    ...
}
フラットに表す場合
{
    "id": 12345,
    "message": "Hello",
    "sender_id": 3456,
    "sender_name": "Taro Yamada",
    "receiver_id": 6789,
    "receiver_name": "Kenji Suzuki",
    ...
}
  • 配列をレスポンスとして返す場合も、オブジェクトで包む
    • レスポンスデータが何を示しているのかわかりやすくなるため
    • レスポンスデータをオブジェクトに統一できるため
    • セキュリティ上のリスクを避けることができるため
      • JSONインジェクションというブラウザが配列の場合読み込んでしまう仕様を悪用した攻撃を軽減できる
  • 配列として取得したデータに続きがある場合は、続きがあるという情報を付与することを考慮する
{
    "timelines": [
        ...
    ],
    hasNext: true
}

データの命名規則

  • 多くのAPIで同じ意味に利用されている一般的な単語を用いる
  • なるべく少ない単語数で表現する
  • 複数の単語を連結する場合、その連結方法はAPI全体を通して統一する
    • いろんな意見があるが、特に主張がなければMozillaやGoogleのスタイルガイドに沿ってキャメルケースで統一する
  • 変な省略形は極力利用しない
    • "timeline"を"tl"、"location"を"lctn"と略さない
  • 単数系/複数形に気をつける
  • 性別は生物学的な性別はsex、社会的・文化的な性別はgenderで設定する
  • 日付のフォーマットはRFC3339を使う
日付例
2015-10-12T11:30:22+09:00

ただし、後述するHTTPヘッダで定義されている日付は異なるフォーマット(RFC882/RFC850/ANSI C)で定義されていることに注意する。

  • 大きな整数(32ビット、約42億種類以上)を扱う場合は、システムや言語で扱えない場合があるため、その際は文字列で数値を扱うことを考慮する
Twitter API例
{
    "id": 266031293949698048,
    "id_str": "266031293949698048",
    ...
}
  • エラー時にはエラーのステータスコードを返却するだけでなく、ボディにもエラー詳細を返却することを検討する
Twitter エラーメッセージ例
{
    "errors": [
        {
            "message": "Bad Authentication data",
            "code": 215
        }
    ]
}
GitHub エラーメッセージ例
{
    "message": "Not Found",
    "documentation_url": "https://developer.github.com/v3"
}

HTTPステータスコード一覧

レスポンスヘッダの先頭に必ず入っている3つの数字であり、処理された結果の概要が示される。
以下のステータスコード表の意味に合わせて、レスポンスヘッダのステータスコードを設計する。

数値1桁での意味合い

ステータスコード 意味
100番台 情報
200番台 成功
300番台 リダイレクト
400番台 クライアントサイドに起因するエラー
500番台 サーバーサイドに起因するエラー

HTTPの主なステータスコード一覧

処理結果に応じて、以下のステータスコードに当てはまるレスポンスとして返す。

ステータスコード 名前 説明
200 OK リクエストは成功した
201 Created リクエストが成功し、新しいリソースが作られた
202 Accepted リクエストは成功した(後続の処理がある、非同期)
204 No Content コンテンツなし(DELETEメソッド成功時に利用を検討するぐらいしか利用用途はない)
300 Multiple Choices 複数のリソースが存在する(データストレージサービスで指定したキーに対して複数データが存在した、など)
301 Moved Permanently リソースは恒久的に移動した
302 Found リクエストしたリソースは一時的に移動している(認証系サービスへのリダイレクトなどで利用)
303 See Other 他を参照(リクエストのメソッドに関わらずGETでリダイレクト)
304 NOt Modified 前回から更新されていない(キャッシュ内容を参照させる)
307 Temporary Redirect リクエストしたリソースは一時的に移動している(POSTからGETヘのリダイレクトを許可しない)
400 Bad Request リクエストが正しくない(他の400番台で表すことができないエラー)
401 Unauthorized 認証が必要(認証エラー)
403 Forbidden アクセスが禁止されている(認可エラー)
404 Not Found 指定したリソースが見つからない
405 Method Not Allowed 指定されたメソッドを使うことができない
406 Not Acceptable Accept関連のヘッダに受理できない内容が含まれている(レスポンスのフォーマットに対応していない)
408 Request Timeout リクエストが時間以内に完了しなかった
409 Conflict リソースが矛盾した(データの登録でIDが重複していたなど)
410 Gone 指定したリソースが消滅した
413 Request Entity Too Large リクエストボディが大きすぎる
414 Request-URL Too Long リクエストされたURLが長すぎる
415 Unsupported Media Type サポートされていないメディアタイプが指定された
429 Too Many Requests リクエスト回数が多すぎる
500 Internal Server Error サーバ側でエラーが発生した
503 Service Unavailable サーバが一時的に停止している

キャッシュの仕様

Expiration Model(期限切れモデル)

レスポンスデータに保存期限を決めておき、期限が切れた場合に再度アクセスをして取得を行う。
HTTP1.1では以下2種類が用意されている。

期限切れモデルヘッダ
Cache-Control: max-age=3600
Expires: Fri, 01 Jan 2016 00:00:00 GMT

同時に設定された場合、Cache-Controlが優先される。Cache-Controlのmax-ageはDataヘッダの時間を参照して計算される。

Validation Model(検証モデル)

今保持しているキャッシュが最新であるかを問い合わせて、データが更新された場合にのみ取得を行う。
条件付きリクエストを行い、更新されていた時のみデータを返し、更新されていなかった場合、304("Not Modified")というステータスコードを返す。
クライアントが保持している情報の状態は、以下の最終更新日付かエンティティタグ(レスポンスデータのハッシュ値)でサーバに伝える。

サーバがレスポンスとして返すヘッダ
Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT
ETag: "ff39b31e285573ee373af0d492aca581"
その後サーバが条件付きリクエストを行う際のヘッダ
If-Modified-Since: Tue, 01 Jul 2014 00:00:00 GMT
If-None=Match: "ff39b31e285573ee373af0d492aca581"

Heuristic Expiration(発見的期限切れ)

サーバ側で明示的な期限が与えられなかった場合のクライアントがおおよそデータをどのくらい保持するかを決めるための方針。Last-Modifiedを見て最新更新が1年前だったためしばらくキャッシュしても問題ない、といった判断である。
許容できるキャッシュの期限はAPIの性質によるため、基本的にはCache-Controlで制御すべきである。

キャッシュさせたくない場合

以下のヘッダを利用する。

no-cache
Cache-Control: no-cache

ヘッダを考慮してキャッシュを制御する場合

HTTPのやり取りがプロキシを経由し、そのプロキシがキャッシュ機能を有する場合に用いられる。Varyヘッダを利用することで、どのリクエストヘッダを使ってキャッシュを行うかを制御できる。
例えば、サーバ駆動型コンテントネゴシエーションを実現するケースで、Accept-Languageヘッダを用いて自然言語を設定する場合、以下のような形でリクエストを受け付ける。

Accept-Language: ja

URIだけでキャッシュを判断した場合、Accept-Languageを別でリクエストした際に本来取りたい自然言語の情報を取得できず、jaの情報が取得されてしまう。
レスポンスヘッダに以下のようなキャッシュに利用するヘッダを指定することで、ヘッダを考慮したキャッシュ制御が可能である。

Vary: Accept-Language

ヘッダ設定

Cache-Controlヘッダ

Cache-Controlヘッダ例
Cache-Control: public, max-age=3600
ディレクティブ名 意味
public キャッシュはプロキシにおいてユーザーが異なっても共有することができる
private キャッシュはユーザーごとに異なる必要がある
no-cache キャッシュしたデータは検証モデルによって確認が必要
no-store キャッシュをしてはならない
no-transform プロキシサーバはコンテンツのメディアタイプやその他内容を変更してはならない
must-revalidate いかなる場合もオリジナルのサーバへの再検証が必要
proxy-revalidate プロキシサーバはオリジナルのサーバへの再検証が必要
max-age データが新鮮である期間を示す
s-maxage max-ageと同様だが中継するサーバでのみ利用される

全員に配信するお知らせ情報や特定地域の転記情報などはpublicでOKだが、/users/meといった自分自身のユーザ情報取得はprivateである必要がある

Content-Typeヘッダ

代表的なメディアタイプ

メディアタイプ データ形式
text/plain プレーンテキスト
text/html HTML文書
application/xml XML文書
text/css CSS文書
application/javascript JavaScript
application/json JSON文書
application/rss+xml RSSフィード
application/atom+xml Atomフィード
application/octet-stream バイナリデータ
application/zip zipファイル
image/jpeg JPEG画像
image/png PNG画像
image/svg+xml SVG画像
multipart/form-data 複数のデータで構成されるウェブフォームデータ
video/mp4 MP4動画ファイル
application/vnd.ms-excel Excelファイル
  • 正しいメディアタイプを設定するようにする
  • 歴史的経緯でtextのタイプがあるが、applicationタイプを利用する方が良い
  • application/x-msgpackなどサブタイプに"x-"で始まるものはIANAに登録されていないものであるが推奨されていない
  • 自分でメディアタイプを定義する場合、"application/vnd.company.awesomeformat"で設定する
  • 利便性だけでなく、セキュリティ面においても正しいメディアタイプを設定すべきである
    • 例えばAPIがtext/htmlをメディアタイプで返してしまう場合
      • ブラウザで画面上に表示する
      • JSONデータに{"data":""}などが入っている
    • の条件が揃うと、それが実行されてしまうため

IANAとは、Internal Assigned Numbers Authorityの略でインターネットに関する番号を管理する組織である。メディアタイプの管理以外にも、ドメインの管理やIPアドレスの割り当ても行なっている。

CORS

異なるオリジンへアクセスする場合、通常はセキュリティ観点でブラウザ上で禁止されている。アクセスしたい場合、プリフライトリクエストを行う必要がある。
※CORSの仕組みは説明すると長くなるのでここでは割愛する。
また別途時間があれば記事を書く。
詳細については以下を参照。
mdn web doc オリジン間リソース共有 (CORS)

X-XSS-Protection

X-XSS-Protection: 1; mode=block

このレスポンスヘッダーがあることでChromeやSafariなどのブラウザが反射型のXSSを発生させそうなパターンをブロックすることができる。
:::message alert
現在はContent-Security-Policy(CSP)を実装することでXSSフィルタリングの必要性が大幅に減少している。現時点では非標準のヘッダになっており、後述のContent-Security-Policyヘッダの使用が推奨される。
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CSP
:::

X-Frame-Options

X-Frame-Options: deny

このレスポンスヘッダーがあることで、クリックジャッキングと呼ばれるIFRAMEを他のページに読み込ませて、クリックさせるような攻撃を防ぐことができる。
:::message alert
こちらも現時点ではContent-Security-Policyヘッダのframe-ancestorsディレクティブを使用している同様の機能をより柔軟に実現することが推奨されている。
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CSP
:::

Content-Security-Policy

コンテンツを何も読み込まない場合
Content-Security-Policy: default-src 'none'
コンテンツを自身のドメインからのみ読み込む場合
Content-Security-Policy: default-src 'self'
コンテンツを自身のドメインと任意のドメインからのみ読み込む場合
Content-Security-Policy: default-src 'self' example.com *.example.com

IMG要素、SCRIPT要素、LINK要素などの読み込み先としてどこを許可するのかを設定できるヘッダである。XSSやデータインジェクション攻撃などのような、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーである。
上記の例のように、コンテンツを特に読み込まない場合や特定のドメインからのコンテンツのみを読み込むなどの制御が可能である。
非常にセキュリティの高い設定ができる一方、制御も大変でありシステム全体に大きな影響を与える可能性がある。本番環境へのスムーズな移行を行うために、report-onlyモードというものがあり、"Content-Security-Policy-Report-Only"ヘッダを設定することで、ブロックは行われず違反があるかの報告のみが行われるため、動作確認にはまずこちらのヘッダの利用を検討すると良い。

Strict-Transport-Security

Strict-Transport-Security: max-age=15768000

このヘッダを利用することで、あるサイトへのブラウザからのアクセスをHTTPSのみに限定させることができる。HTTPの通信が行われた際にHTTPSにリダイレクトする、という手法がよく使われるが、その際の問題として初回が暗号化されていないので、中間車攻撃などによって書き換えられる危険性が高くなっている。このヘッダを設定しておくことで、max-age期間中はブラウザ側で記録してことができ、2回目以降はHTTPで送られてもHTTPSのアクセスに変換される。

Set-Cookie

Set-Cookie: session=e827ea0c...; Path=/; Secure; HttpOnly

ブラウザでセッションを扱う場合にクッキーをセッション管理に使うケースが多い。その際のセキュリティ強化のためにSecure、HttpOnly属性を付与する。
Secure属性を付与するとクッキーはHTTPS時通信のみ、サーバに送り返すようになる。これによりセッション情報は暗号化された通信のみでサーバとやり取りできるようになる。
HttpOnly属性を付与するとクッキーはブラウザ上でJavaScriptなどのスクリプトを使ってアクセスできないようにすることができる。これによりXSSなどによって、そのクッキーに含まれたセッション情報が読み出されることを防止することができる。

X-Requested-With

X-Requested-With: XmlHttpRequest

XSSやXSRF(Cross Site Request Forgery)の攻撃を防ぐ手段の一つとして利用する独自ヘッダである。このヘッダがリクエストに存在しなかった場合、APIを提供するサーバでアクセス拒否する。前の章で紹介したGETメソッドではサーバ側のデータを変化させない設計をしたうえで、同一生成元ポリシーの影響を受けないGETメソッド以外の、FORM要素からのPOSTメソッドのアクセスも防ぐことができる。
(ブラウザからのFORMによるPOSTアクセスはヘッダを独自に追加できないため)
なお、CORSの仕組みを利用してAPIを提供するならば、プリフライトリクエストの際にこのヘッダも明示的に許可する必要がある。

独自のHTTPヘッダを設定する場合

  • "X-"という接頭辞をつける習慣があった
    • X-GitHub-Request-Id: ...
    • x-li-request-id: ...
  • 近年ではX-を使うことはRFC6648から、お勧めできない状態であるが、付与されたいた方がわかりやすい意見もあり、議論が難しいものである
  • どちらにしても、X-をつけるかつけいないかは、サービスとしては統一する

運用設計

APIのバージョン管理

https://graph.facebook.com/v2.0/me
http;//api.linkedin.com/v1/people
https://api.dropbox.com/a/account/info

APIにはバージョン番号をパスに付与し、互換性がないバージョンアップが発生した場合、別エンドポイントとして提供するようにする。
なお、マイナーバージョンアップは基本的に互換性があることが前提のケースが多いため、パスで管理するバージョンはメジャーバージョンのみで良い。

大量アクセスへの対策

Web上で公開するため、DoS攻撃といった大量のアクセスに備える必要があり、そのためにAPIは以下のような対策を行う必要がある。

  • APIは提供するユーザやシステムを特定し、それぞれにアクセス制限を行う
  • レートリミットとして1時間に100回などの制限値を設ける
  • 制限値を超えた場合、429エラーをレスポンスし、可能であれば、Retry-Afterヘッダで具体的に再アクセスが可能な時間を明示する
  • ドキュメントに制限回数を明示したり、API利用者向けのダッシュボードにて情報を提供することも検討する

APIドキュメントの提供

APIのドキュメントは利用者に公開し、バージョンアップなどが発生した場合も反映するようにする。OpenAPIなどのフォーマットを利用すれば、ドキュメントの自動生成も可能で利用するクラウドやフレームワークによっては、API定義ファイルとしても利用できる。
更新が煩雑にならないような仕組みを検討する。

サンドボックスAPIの提供

決済処理などの金銭授受が発生するようなAPIは、本番環境で試すには大きな影響のある。こういったAPIを提供する際はサンドボックス環境を用意し、ユーザがテストを行いやすいようサービスを展開する。

最後に

最後まで読んでいただきありがとうございます。
本の内容を元に一部追記をしたり、記述の構成を変えたりしました。
もちろんこのケースが全てのプロダクトに当てはまるわけではないので、ドメインやサービスの特性、ユーザの環境などを加味したうえで設計方針の採用を検討する必要があると思います。
正直まだまだ奥が深い分野ではありますが、この部分さえ押さえればそこまで悪いAPI設計にはならないと信じています。
それでは。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?