Go to list of users who liked
More than 5 years have passed since last update.
Deno🦕でRESTfulなAPIサーバーを立ててCloud Runへデプロイしてみる
Denoの学習用にRESTfulなAPIサーバーを作ってみたのでまとめます。
今回作るもの
DenoとServestを使ってRESTfulなAPIサーバーを作り、GCPのCloud Runへデプロイするまでです。
(GIFではcURLで実行していますが、もちろんプラウザでも確認できます)
今回記載するサンプルコードは全て以下GitHubリポジトリにあります。もし、動かなければこちらを参照してみてください。
Denoとは?
Denoは、Node.jsに変わる新しいJavaScriptのランタイムです。
ネイティブでTypeScriptをサポートしています。また、package.json
での依存管理は不要で、URLから直接パッケージをimportしてキャッシュに保存します。ローカルに巨大なnode_modules
を持つ必要もないです。
その他詳細な情報は以下にとても分かりやすく記載されていました。
- The Deno Handbook: A TypeScript Runtime Tutorial with Code Examples
- Denoとはなにか - 実際につかってみる - Qiita
- Denoの登場でNode.jsの時代は終わるのか? - Qiita
Hello Deno 🦕
最初にDenoのインストールから、簡単なスクリプトの実行までを行います。
まず、HomebrewでDenoをインストールします。
brewinstalldeno
これだけで完了です。
早速、実行してみましょう。
$deno run https://deno.land/std/examples/welcome.tsWelcome 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を使います。
任意のディレクトリにmain.tsを作りましょう。
import{createApp,}from"https://servestjs.org/@v1.0.0/mod.ts";constapp=createApp();app.handle("/",async(req)=>{awaitreq.respond({status:200,headers:newHeaders({"content-type":"text/plain",}),body:"hello World!",});});app.listen({port:8888});
実行してみます。
$deno run--allow-net app/main.tsI[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
を作成しましょう。
import{v4}from"https://deno.land/std/uuid/mod.ts";exportclassPost{publicid:string;publiccreatedAt:stringconstructor(publictitle:string,publiccontent:string){this.id=v4.generate();this.createdAt=(newDate()).toLocaleDateString()}toJson(){return{id:this.id,title:this.title,content:this.content,createdAt:this.createdAt};}}// DBでの永続化はせずインメモリで管理exportconstposts=[]asPost[];// postsの初期記事追加exportconstinitializedPosts=()=>{posts.push(newPost('Hello Blog','My first post.'))}initializedPosts();
記事のモデルとなるPostクラスを定義しています。
今回は説明を簡易にするため、インメモリでpostsを保持し、最初のサンプル記事を追加する処理を書いています。
Controllerの作成
続いて、リクエストごとのハンドリングを行うControllerを作成します。app/controllers
にpostController.ts
を作成します。
import{ServerRequest,}from"https://servestjs.org/@v1.0.0/mod.ts";import{posts,Post}from"../models/post.ts";typePostPayload={id:string;title:string;content:string;};exportconstgetAllPosts=async(req:ServerRequest)=>{awaitreq.respond({status:200,headers:newHeaders({"content-type":"application/json",}),body:JSON.stringify(posts),});};exportconstgetPost=async(req:ServerRequest)=>{const[_,id]=req.match;awaitreq.respond({status:200,headers:newHeaders({"content-type":"application/json",}),body:JSON.stringify(posts.find((p)=>p.id===id)),});};exportconstcreatePost=async(req:ServerRequest)=>{constbodyJson=(awaitreq.json())asPostPayload;constpost=newPost(bodyJson.title,bodyJson.content);posts.push(post);awaitreq.respond({status:200,headers:newHeaders({"content-type":"application/json",}),body:JSON.stringify(post),});};exportconstdeletePost=async(req:ServerRequest)=>{const[_,id]=req.match;constindex=posts.findIndex((p)=>p.id===id);posts.splice(index,1);awaitreq.respond({status:204,});};exportconstupdatePost=async(req:ServerRequest)=>{const[_,id]=req.match;constbodyJson=(awaitreq.json())asPostPayload;constindex=posts.findIndex((p)=>p.id===id);posts[index].title=bodyJson.title;posts[index].content=bodyJson.content;awaitreq.respond({status:200,headers:newHeaders({"content-type":"application/json",}),body:JSON.stringify(posts[index]),});};
先ほど作成したPostモデルから、posts
をimportしてそれぞれの関数でpostsを処理しています。
関数名の通り、記事のCRUD処理を行っています。updatePost
やdeletePost
では、const [_, id] = req.match;
でリクエストのURLパラーメータから、idを取得しています。
レスポンスは、req.respond()
でstatus
、headers
、body
を持つオブジェクトを返すだけです。
Routerの作成
次に、ControllerをURLにマッピングするRouterを作成します。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";exportconstroutes=()=>{constrouter=createRouter();router.get("posts",getAllPosts);router.get(newRegExp("^posts/(.+)"),getPost);router.post("posts",contentTypeFilter("application/json"),createPost);router.put(newRegExp("^posts/(.+)"),contentTypeFilter("application/json"),updatePost,);router.delete(newRegExp("^posts/(.+)"),deletePost);returnrouter;};
createRouter()
でrouterのインスタンスを作成して、router.get(..)
などでURLと、処理をマッピングしています。
処理自体は、先ほど書いたControllerからimportした関数です。
routerの関数名が、post, delete, putなどのHTTPリクエストに対応しています。
new RegExp("^posts/(.+)")
で正規表現でリクエストを絞ることも可能です。
ここでは、記事更新のPUTリクエストと、記事削除のDELETEリクエストでURLパラメータを受け取るために利用しています。
Appの作成
最後に、main.ts
でAppのエントリーポイントを作成します。
最初のHello worldを以下の用に書き換えましょう。
import{createApp}from"https://servestjs.org/@v1.0.0/mod.ts";import{routes}from"./router.ts";constapp=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はこちらのリポジトリのものです。
FROM hayd/alpine-deno:1.0.1ENV PORT 8080ENV HOST 0.0.0.0WORKDIR /appUSER denoCOPY . .CMD ["run", "--allow-net", "--allow-env", "app/main.ts"]
次に、cloud-runの設定ファイルcloud-build.ymlを作成します。
steps:-name:gcr.io/cloud-builders/dockerargs:['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が登録されているはずです。

そして、登録したコンテナを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コンソールも確認してみましょう。

無事アクセス出来ましたか?
これでCloud Runへのデプロイも完了です 🎉
終わりに
以上、「Deno🦕でRESTfulなAPIサーバーを立ててCloud Runへデプロイ」でした。本当に触りまでしか使ってませんが、Denoはとても可能性感じますね。
今後もちょこちょこ使っていきたいです。
Denoで困ったときは、日本のDenoコミュニティのSlackに参加し質問するととても親切に教えてくれるのでこちらもおすすめです。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme