80
72

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 3 years have passed since last update.

Deno🦕でRESTfulなAPIサーバーを立ててCloud Runへデプロイしてみる

Last updated at Posted at 2020-05-23

Denoの学習用にRESTfulなAPIサーバーを作ってみたのでまとめます。

今回作るもの

DenoとServestを使ってRESTfulなAPIサーバーを作り、GCPのCloud Runへデプロイするまでです。

May-23-2020 22-16-25.gif
(GIFではcURLで実行していますが、もちろんプラウザでも確認できます)

今回記載するサンプルコードは全て以下GitHubリポジトリにあります。もし、動かなければこちらを参照してみてください。

スクリーンショット 2020-05-23 21.47.55.png

Denoとは?

Denoは、Node.jsに変わる新しいJavaScriptのランタイムです。
ネイティブでTypeScriptをサポートしています。また、package.json での依存管理は不要で、URLから直接パッケージをimportしてキャッシュに保存します。ローカルに巨大な node_modules を持つ必要もないです。

その他詳細な情報は以下にとても分かりやすく記載されていました。

Hello Deno 🦕

最初にDenoのインストールから、簡単なスクリプトの実行までを行います。

まず、HomebrewでDenoをインストールします。

brew install deno

これだけで完了です。
早速、実行してみましょう。

$ deno run https://deno.land/std/examples/welcome.ts

Welcome to Deno 🦕

「Welcome to Deno 🦕」 と表示されれば成功です。
ちなみにwelcome.tsの中身は、console.log('Welcome to Deno 🦕') だけです。
https://deno.land/std/examples/welcome.ts

ServestでHello World!

今回はstdのHttpライブラリではなく、DenoでプログレッシブなHTTPサーバーを構築することができるServestを使います。

スクリーンショット 2020-05-23 21.49.47.png

任意のディレクトリにmain.tsを作りましょう。

app/main.ts
import {
  createApp,
} from "https://servestjs.org/@v1.0.0/mod.ts";
const app = createApp();
app.handle("/", async (req) => {
  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "text/plain",
    }),
    body: "hello World!",
  });
});
app.listen({ port: 8888 });

実行してみます。

$ deno run --allow-net app/main.ts

I[2020-05-23T12:13:59.669Z] servest:router listening on :8888

http://localhost:8888 にアクセスすると、

hello world と表示されるはずです。

コードをみると、createApp()でappを作り、app.handle("/", async ...)でリクエストをハンドリング、req.respond({...}) でレスポンスを返しています。
そして、app.listen({ port: 8888 }) でport:8888でリクエストを待っているのが分かります。
Node.jsのExpressのような書き味ですね。

RESTfulなAPIサーバーの構築

簡単なブログ投稿システムをイメージして、RESTfulなAPIサーバーを作ってみます。

Modelの作成

まずModelを作ります。
app/modelsに以下post.tsを作成しましょう。

app/model/post.ts
import { v4 } from "https://deno.land/std/uuid/mod.ts";

export class Post {
  public id: string;
  public createdAt: string
  constructor(public title: string, public content: string) {
    this.id = v4.generate();
    this.createdAt = (new Date()).toLocaleDateString()
  }

  toJson() {
    return {
      id: this.id,
      title: this.title,
      content: this.content,
      createdAt: this.createdAt
    };
  }
}

// DBでの永続化はせずインメモリで管理
export const posts = [] as Post[];

// postsの初期記事追加
export const initializedPosts = () => {
  posts.push(new Post('Hello Blog', 'My first post.'))
}

initializedPosts();

記事のモデルとなるPostクラスを定義しています。
今回は説明を簡易にするため、インメモリでpostsを保持し、最初のサンプル記事を追加する処理を書いています。

Controllerの作成

続いて、リクエストごとのハンドリングを行うControllerを作成します。
app/controllerspostController.tsを作成します。

app/controllers/postController.ts
import {
  ServerRequest,
} from "https://servestjs.org/@v1.0.0/mod.ts";
import { posts, Post } from "../models/post.ts";

type PostPayload = {
  id: string;
  title: string;
  content: string;
};

export const getAllPosts = async (req: ServerRequest) => {
  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "application/json",
    }),
    body: JSON.stringify(posts),
  });
};

export const getPost = async (req: ServerRequest) => {
  const [_, id] = req.match;
  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "application/json",
    }),
    body: JSON.stringify(posts.find((p) => p.id === id)),
  });
};

export const createPost = async (req: ServerRequest) => {
  const bodyJson = (await req.json()) as PostPayload;
  const post = new Post(bodyJson.title, bodyJson.content);
  posts.push(post);

  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "application/json",
    }),
    body: JSON.stringify(post),
  });
};

export const deletePost = async (req: ServerRequest) => {
  const [_, id] = req.match;
  const index = posts.findIndex((p) => p.id === id);
  posts.splice(index, 1);

  await req.respond({
    status: 204,
  });
};

export const updatePost = async (req: ServerRequest) => {
  const [_, id] = req.match;
  const bodyJson = (await req.json()) as PostPayload;
  const index = posts.findIndex((p) => p.id === id);

  posts[index].title = bodyJson.title;
  posts[index].content = bodyJson.content;

  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "application/json",
    }),
    body: JSON.stringify(posts[index]),
  });
};

先ほど作成したPostモデルから、postsをimportしてそれぞれの関数でpostsを処理しています。
関数名の通り、記事のCRUD処理を行っています。
updatePostdeletePostでは、const [_, id] = req.match; でリクエストのURLパラーメータから、idを取得しています。

レスポンスは、req.respond()statusheadersbodyを持つオブジェクトを返すだけです。

Routerの作成

次に、ControllerをURLにマッピングするRouterを作成します。
app/router.tsを作成しましょう。

app/router.ts
import {
  createRouter,
  contentTypeFilter,
} from "https://servestjs.org/@v1.0.0/mod.ts";
import {
  getAllPosts,
  getPost,
  createPost,
  deletePost,
  updatePost,
} from "./controllers/postsController.ts";

export const routes = () => {
  const router = createRouter();

  router.get("posts", getAllPosts);
  router.get(new RegExp("^posts/(.+)"), getPost);
  router.post("posts", contentTypeFilter("application/json"), createPost);
  router.put(
    new RegExp("^posts/(.+)"),
    contentTypeFilter("application/json"),
    updatePost,
  );
  router.delete(new RegExp("^posts/(.+)"), deletePost);

  return router;
};

createRouter()でrouterのインスタンスを作成して、router.get(..) などでURLと、処理をマッピングしています。
処理自体は、先ほど書いたControllerからimportした関数です。
routerの関数名が、post, delete, putなどのHTTPリクエストに対応しています。

new RegExp("^posts/(.+)") で正規表現でリクエストを絞ることも可能です。
ここでは、記事更新のPUTリクエストと、記事削除のDELETEリクエストでURLパラメータを受け取るために利用しています。

Appの作成

最後に、main.tsでAppのエントリーポイントを作成します。
最初のHello worldを以下の用に書き換えましょう。

main.ts
import { createApp } from "https://servestjs.org/@v1.0.0/mod.ts";
import { routes } from "./router.ts";

const app = createApp();

app.route("/", routes());
app.listen({ port: 8080 });

createApp()でAppを作成し、app.route()で先ほど定義したrouterを渡してURLをマッピングしています。
これで準備は完了です。

実行

サーバーを起動してみます。

$ deno run --allow-net src/main.ts

以下でCRUD処理を試してみてください。

記事一覧の取得

$ curl --request GET \
  --url http://localhost:8080/posts

個別記事の取得

$ curl --request GET \
  --url http://localhost:8080/posts/{POST_ID}

記事の投稿

$ curl --request POST \
  --url http://localhost:8080/posts \
  --header 'content-type: application/json' \
  --data '{"title": "sample","content": "Laborum mollit duis ad consequat."}'

記事のアップデート

$ curl --request PUT \
  --url http://localhost:8080/posts/{POST_ID} \
  --header 'content-type: application/json' \
  --data '{"title": "update","content": "Labore minim sit et id aliquip ad voluptate nisi mollit incididunt id irure enim."}'

記事の削除

$ curl --request DELETE \
  --url http://localhost:8080/posts/{POST_ID}

無事RESTfulなAPIサーバーが作成できましたね 🎉

Cloud Runへのデプロイ

最後にここまでで作ったアプリをCloud Runにデプロイします。
Cloud Runはサーバーレスでフルマネージドなコンテナ実行環境です。
以降の説明は、gcloud cliや、GCPプロジェクトの作成は完了している前提で進めます。

まず、ワークスペースにDockerfileを作成します。
ベースとなるImageはこちらのリポジトリのものです。

スクリーンショット 2020-05-24 14.50.21.png

FROM hayd/alpine-deno:1.0.1

ENV PORT 8080
ENV HOST 0.0.0.0

WORKDIR /app

USER deno

COPY . .

CMD ["run", "--allow-net", "--allow-env", "app/main.ts"]

次に、cloud-runの設定ファイルcloud-build.ymlを作成します。

cloud-build.yml
steps:
  - name: gcr.io/cloud-builders/docker
    args: ['build','-f','Dockerfile','-t','gcr.io/qiita-demo-project/deno-example','.']
images: ['gcr.io/qiita-demo-project/deno-example']

qiita-demo-project の部分は、GCPのプロジェクト名。
deno-exampleの部分は、今回Container Repositoryへ登録するコンテナ名です。

あとはgcloud cliを使ってデプロイします。

まずワークスペースでプロジェクトの初期化を行います。
initで設定するプロジェクトは、先ほどcloud-build.ymlに書いたプロジェクト名を選択してください。

$ gcloud auth login
$ gcloud init

gcloudのプロジェクト設定が終わったらいよいよデプロイ作業です。

まず、gcloud buildコマンドで、Docker ImageをContainer Repositoryに登録します。

# --projectの部分は適宜変更してください
$ gcloud builds submit --project "qiita-demo-project" --config=./cloud-build.yml

成功すると、GCPのContainer RepositoryにImageが登録されているはずです。

スクリーンショット 2020-05-24 18.16.00.png

そして、登録したコンテナをCloud Runにデプロイします。

# --project, --imageの部分は適宜変更してください
$ gcloud beta run deploy qiita-demo \
  --region us-central1 --allow-unauthenticated --project \
  "qiita-demo-project" --image gcr.io/qiita-demo-project/deno-example

target platformの選択肢が出たら [1] Cloud Run (fully managedを選択してください。
成功すると最後にURLが出力されるはずです。

GCPのCloud Runコンソールも確認してみましょう。

スクリーンショット 2020-05-24 18.16.37.png

無事アクセス出来ましたか?
これでCloud Runへのデプロイも完了です 🎉

終わりに

以上、「Deno🦕でRESTfulなAPIサーバーを立ててCloud Runへデプロイ」でした。本当に触りまでしか使ってませんが、Denoはとても可能性感じますね。
今後もちょこちょこ使っていきたいです。

Denoで困ったときは、日本のDenoコミュニティのSlackに参加し質問するととても親切に教えてくれるのでこちらもおすすめです。

80
72
2

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
80
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?