やりたいこと
API は、案件ごとに実装方法が変わったりしないので、何度も共通部分を実装しなおしたり、コピペするのは時間の無駄。
API を作成する案件では、API だけでなく API Documents も納品する必要があったりする。openapi.yaml だけ納品して、後は好きにして。で OK なことがあんまりない。ブラウザでアクセスできる状態で納品してと言われる。Basic認証付きで。サーバも用意されてないのに。なんならモックも用意してと言われる。サーバもないのに。
を解決したい。
- Bearer 認証付き API の基本系を準備する(
Next.js
,TypeScript
,Prisma
) - API の基本形をボイラープレートする(
scaffdog
) - openapi.yaml を元にした API Documents を Basic 認証かけて組み込む (
@stoplight/elements
,Next.js12::middleware
) - docker で動くようにする
できたもの
システム構成
- Next.js: 12.2+
- Prisma: 4.x
Bearer 認証付き API の基本形を準備する
各種バージョンが違うが、これを基本にする。Prisma のメジャーバージョンが上がっているが、今回の内容程度ではほぼ影響がない。
どこまで作るか?は悩ましいが、テンプレートなので、API としては、
- [POST] auth/register : ユーザ登録
- [POST] auth/access_token : アクセストークンを取得
- [GET] users : ユーザ一覧を取得
- [GET, PUT, DELET] users/{userId} : ユーザ情報を取得・更新・削除
これくらい作っておくことにした。認証は Json Web Token を使っているが、もっと変えても良い。テンプレートなので、いったん JWT のままにしておく。
├── handlers // Cors, Bearer 認証
├── lib // 共通で使うもの
├── pages
│ └── api // 具体的な API
├── prisma // schema 定義
└── types // type
以前の投稿で説明が足らないなと思うのは、relation だが、今回の内容とはずれるので、別の機会に。
scaffdog で一括生成する
API のエンドポイントは、どのようにするのが分かりやすいか?使うメソッドは何が良いのか?など、正解はない気がするが、重要なのは、分かりやすさであり、一貫性であると思う。
という訳で、API の基本形は、例えば、articles API を作る場合、
pages/api
└── articles
├── index.ts
└── [articleId].ts
を作り、
- index.ts
- GET: 一覧を取得
- POST: 登録
- [articleId].ts
- GET: 詳細を取得
- PUT: 更新
- DELETE: 削除
とする。レスポンスは、
- 共通
- 400 Bad Request (バリデーションエラー)
- 401 Unauthorized (認証エラー)
- 406 Not Acceptable (認証エラー)
- 500 Internal Server Error (システムエラー)
- GET
- 200 OK (正常系)
- 204 No Content (0件など)
- 404 Not Found (slug に該当するレコードがない場合)
- POST, PUT
- 200 OK (正常系)
- 404 Not Found (親・対象がない場合)
- DELETE
- 204 No Content (正常系)
とした。201 とか 409 とかあるけど、いったんこれで。
こういう基本形があると、openapi.yaml を書く時も楽。
scaffdog は、マークダウンでひな形を書くツールで、一括で複数ファイルを作ってくれる。使い方は簡単で、ドキュメントで十分な情報が得られる。
$ yarn scaffdog generate
と打つと、対話式で作成してくれる。
これをやる前(後でも良いけど)に、Prisma の model を作成しておく必要があり、上記の例だと、Article
という名前の model を作成する。
model Article {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("articles")
}
こんな感じで。
特筆すべきことはあまりないが、
slug は、プログラムで取得すると、string で取れるので、where にそのまま入れると TypeError になるため、どっちなの?と聞いて処理を分けている。
また、上記例では、types/ArticleType.ts
というファイルができているが、relation を実装していく上で必要になるため、型定義をして組み込んでいる。relation がない場合、必要ないが relation が全くないものもあんまりないので、作っておいた。
import { Prisma } from '@prisma/client';
import type { Article } from '@prisma/client';
export type ArticleResponseType = Article & {}
export type ArticlePostPropsType = Prisma.ArticleCreateInput & {}
export type ArticlePutPropsType = Prisma.ArticleUpdateInput & {}
こんなのができる。{}
に、relation で使うものを書いていくことになる。今回は書かない。
コマンド打つだけで、素の API は出来上がる。ここまで決まっていればコピペ + 置換でも実際は大差ないが。大差ないので、こっちのほうが良い。
API Documents を組み込む
openapi は、stoplight studio などを使って書くもよし、自分で書くもよし。好きに書いていただいて良い。小規模な API の場合は、ツールを使って書くのが良いが、大規模になってくると、手動で書くほうが良い。気がする。
/docs
の下に openapi.yaml が置いてあり、dist
ディレクトリがあるが、dist に merge したものを吐き出すつもりで、そんな構成になっている。merge は、 /scripts/build_openapi.sh
を叩けば merge される。
API Documents は、localhost:3000/docs にアクセスしたらベーシック認証がかかった状態で、表示されるようにしたい。
openapi.yaml から API Documents を生成するモジュールはいくつかあるが、今回は、@stoplight/elements
を利用した。モックで Prism 使うし、stoplight studio も使うし、揃えちゃって良いかなと思って。
yarn dev すると Warning が出たり、オプションがきちんと動かなかったり、あんまりお勧めはしないが、build すれば問題ないし、Production には出さないつもりなので、良いことにした。
こういうのが出る。stoplight studio と見た目が違うのかよ!と思ったけど。
実装は簡単で、
import type { NextPage } from 'next';
import React from 'react';
import Head from 'next/head';
import { API } from '@stoplight/elements';
import '@stoplight/elements/styles.min.css';
const ApiDocuments: NextPage = () => {
return (
<div>
<Head>
<title>API Documents</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<API
apiDescriptionUrl="/elements/ZJ6wMNW4Md4NEu.yaml"
basePath="/docs"
router="memory"
/>
</div>
);
};
export default ApiDocuments;
これだけ。
<API
apiDescriptionUrl="/elements/ZJ6wMNW4Md4NEu.yaml"
basePath="/docs"
router="memory"
/>
ここの、apiDescriptionUrl
は、 /public/elements
に入っている。/docs/dist/
は参照できないため、public に入れた。public ディレクトリなので、何となく分かりにくい名前が良いかなと、こんな名前になっている。
/scripts/build_openapi.sh
を叩けば、public の方にも吐き出される。
router
は、memory 以外ではうまく動かなかった。
@stoplight/elements は活発に開発が行われているので、そのうち直るんじゃないかと。
Basic 認証
次に、Basic 認証。Next.js では、middleware を使って Basic認証するが、バージョンが 12 になって Middleware の扱いが大きく変わった。詳細は割愛するが、いままでは pages の下などにあった _middleware.ts がルートディレクトリに1つだけ middlewre.ts を置くようになった。詳細はこちら。
Next.js v12 で Basic 認証は、以下のように書く。
import { NextRequest, NextResponse } from 'next/server';
export const config = {
matcher: '/docs',
};
export function middleware(req: NextRequest) {
const basicAuth = req.headers.get('authorization');
const url = req.nextUrl;
if (basicAuth) {
const authValue = basicAuth.split(' ')[1];
const [user, password] = atob(authValue).split(':');
if (
user === process.env.BASIC_AUTH_USER &&
password === process.env.BASIC_AUTH_PASSWORD
) {
return NextResponse.next();
}
}
url.pathname = '/api/basicAuth';
return NextResponse.rewrite(url);
}
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(_: NextApiRequest, res: NextApiResponse) {
res.setHeader('WWW-authenticate', 'Basic realm="Secure Area"');
res.statusCode = 401;
res.end(`Auth Required.`);
}
なお、
const [user, password] = atob(authValue).split(':');
この部分は、atob は非推奨だから
const [user, password] = Buffer.from(authValue).toString().split(':');
こう書けと言われるが、こう書くと、
./middleware.ts
A Node.js API is used (Buffer at line: 10) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
と出る。残念。
docker で動かす
最後に docker であるが、docker は特別変わったことはやっていないが、Prisma で binaryTarget を追加しないと動かないことがある。
generator client {
provider = "prisma-client-js"
previewFeatures = ["interactiveTransactions"]
binaryTargets = ["native", "linux-musl"]
}
...
リナックスまっする!
使い方
テンプレートなので。
$ npx create-next-app some_api --example https://github.com/TAKT-R-D/next-ts-api-template
便利!!
やらなかったものなど
dredd とは仲良くなれなかったので、今回は諦めた。
mock サーバは、Prism を使って立ち上げるスクリプトを /scripts/run_moch.sh
においたが、コンテナは作らなかった。
また、当初、openapi.yaml から API を自動生成する方向で考えていたが、結局辛いのは、openapi.yaml の方なので、そっちを自動生成する方向で再検討する。scaffdog でテンプレート書けばできちゃいそうだけど。そこまでできれば、API 作成は、基本 schema の定義するだけでできるようになる。
最後に
基本的に作業に落とし込んだものは、自分では実装せず、メンバーに依頼するのだが、「articles api を参考に実装して」という頼み方をすると、なんか書き方を変えてきたり、いや、俺は 409 が入ってた方が良いと思う、などと別の処理を入れてきたりする。レビューが面倒だし、テストも必要になる。API 仕様書も書き換えないといけなくなるし、作業が増える。増えるというか、このまんま使ってもらえれば、レビューもテストも基本必要なくなるのが良いところなので、極力アレンジの余地がないようにしたい。更にここまで出来ていれば依頼するメンバーはエンジニアである必要がない。掃除のおばちゃんでもできるし、中学生の息子でもできる。
もちろん、これで完成はしないので、エンジニアが不要にはならないが、世界中のエンジニアの1%の工数が削減できれば、それは20万人分の工数に匹敵するので、まぁ100万人分くらいは削減できるかもしれない笑。会社的には、相当なコストカットになる。
弊社は、そんなようなことをやっている会社です。てっていてきに。
以上、おしまい。