9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.js + typescript + Prisma の API テンプレートを作る

Last updated at Posted at 2022-08-14

やりたいこと

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 の基本形を準備する

Next.js(TypeScript) で Prisma を使って 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

と打つと、対話式で作成してくれる。

Screen Shot 2022-08-15 at 2.47.19.png

これをやる前(後でも良いけど)に、Prisma の model を作成しておく必要があり、上記の例だと、Article という名前の model を作成する。

schema.prisma
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 が全くないものもあんまりないので、作っておいた。

types/ArticleType.ts
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 には出さないつもりなので、良いことにした。

Screen Shot 2022-08-15 at 1.37.34.png

こういうのが出る。stoplight studio と見た目が違うのかよ!と思ったけど。

実装は簡単で、

/pages/docs/index.tsx
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 認証は、以下のように書く。

/middleware.ts
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);
}
/pages/api/basicAuth.ts
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 を追加しないと動かないことがある。

schema.prisma
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万人分くらいは削減できるかもしれない笑。会社的には、相当なコストカットになる。

弊社は、そんなようなことをやっている会社です。てっていてきに。

以上、おしまい。

9
6
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
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?