41
19

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.

NimAdvent Calendar 2020

Day 14

Nim製ウェブフレームワーク一挙大解説!

Last updated at Posted at 2020-12-13

昨年やっとV1.0がリリースされたNim言語ですが、今年はv1.4.2まで開発が進みました。
Nim自体も徐々に人気を集めてきて、それに伴いウェブフレームワークも沢山作られるようになりました。
今回は群雄割拠のNim製のウェブフレームワークについて網羅的に解説していきます!

比較するために、以下の全て同じシナリオを実装したいと思います。

Httpメソッド URI リクエスト 期待するレスポンス
GET / <h1>Hello World</h1>
POST / {"name": "John"} {"message": "Hello John"}
GET /{id} {id}
サーバーは8000番ポートで立ち上げます。
POSTでアクセスされた時、JSONリクエストから値を取り出すのに失敗した時には400を返します。
HttpメソッドやURIが一致しない場合には404を返します。

asynchttpserver

asynchttpserverは非同期通信するアプリケーションサーバーを作るためのライブラリで、Nim標準ライブラリに入っています。

import asynchttpserver, asyncdispatch, re, strutils, json

proc main {.async.} =
  var server = newAsyncHttpServer()
  proc cb(req: Request) {.async.} =
    if req.reqMethod == HttpGet and req.url.path == "/":
      let headers = {"Content-type": "text/html; charset=utf-8"}
      await req.respond(Http200, "<h1>Hello World</h1>", headers.newHttpHeaders())

    elif req.reqMethod == HttpPost and req.url.path == "/":
      let headers = {"Content-type": "application/json; charset=utf-8"}
      try:
        let requestBody = req.body.parseJson
        let name = requestBody["name"].getStr
        let response = %*{"message": "Hello " & name}
        await req.respond(Http200, $response, headers.newHttpHeaders())
      except Exception:
        let response = %*{"message": "エラーが発生しました"}
        await req.respond(Http400, $response, headers.newHttpHeaders())

    elif req.reqMethod == HttpGet and req.url.path.match(re"\/\d"):
      let headers = {"Content-type": "text/html; charset=utf-8"}
      let id = req.url.path.split("/")[1]
      await req.respond(Http200, id, headers.newHttpHeaders())

    else:
      await req.respond(Http404, "Not found")

  waitFor server.serve(Port(8000), cb)

discard main()

Request型を引数に取るコールバック関数を作り、それをserver.serve()の第二引数に入れてあげることでサーバーが起動します。
Request型に様々なメソッドが用意されており、そのメソッドを使うことでURLパスやHttpメソッドやリクエストボディを取り出せるようになっています。
標準ライブラリなのでリッチなルーティングも用意されておらず、自前で実装しなければなりませんが、必要最小限の機能はあるかなと思います。

httpbeast

httpbeastはNimのコミッターとして活発に活動されているdom96氏が作ったライブラリです。マルチスレッドで高速に動作するアプリケーションサーバーを作ることができます。
後述するjesterはこのhttpbeastをベースにして作られています。

import options, asyncdispatch, json, re, strutils
import httpbeast

proc onRequest(req: Request):Future[void] =
  if req.httpMethod == some(HttpGet) and req.path.get() == "/":
    let headers = "Content-type: text/html; charset=utf-8"
    req.send("<h1>Hello World</h1>")

  elif req.httpMethod == some(HttpPost) and req.path.get() == "/":
    let headers = "Content-type: application/json; charset=utf-8"
    try:
      let requestBody = req.body.get.parseJson
      let name = requestBody["name"].getStr
      let response = %*{"message": "Hello " & name}
      req.send(Http200, $response, headers)
    except Exception:
      let response = %*{"message": "エラーが発生しました"}
      req.send(Http400, $response, headers)

  elif req.httpMethod == some(HttpGet) and req.path.get.match(re"\/\d"):
    let headers = "Content-type: text/html; charset=utf-8"
    let id = req.path.get.split("/")[1]
    req.send(Http200, id, headers)

  else:
    req.send(Http404)

let settings = initSettings(Port(8000))
run(onRequest, settings)

何もオプションを付けずに動かすと1 threadで動きますが

nim c -r http_beast.nim
>Starting 1 threads

コンパイルオプションに--threads:on を付けることで実行するPCのコア数分マルチスレッドで動いてくれます。

nim c -r --threads:on http_beast.nim
>Starting 4 threads
>Listening on port 8000

jester

jesterはhttpbeastをベースにして、さらにプロダクションユースでより使いやすくしたマイクロウェブフレームワークです。Nim開発者のためのコミュニティであるNim ForumはこのJesterで動いています。
Nimのウェブフレームワークとしては最も選択肢に上がるかなと思いますが、非同期処理には対応していないところが個人的には玉に瑕かなという印象があります。
しかしレスポンスの速さはGoもRustも凌駕しており、一目置くものがあります。

スクリーンショット 2020-12-13 16.52.07.jpg

スクリーンショット 2020-12-13 16.52.30.jpg

import jester, json

router route:
  get "/":
    resp "<h1>Hello World</h1>"

  post "/":
    try:
      let params = request.body.parseJson
      let name = params["name"].getStr
      let response = %*{"message": "Hello " & name}
      resp response
    except Exception:
      let headers = [("Content-type", "application/json; charset=utf-8")]
      let response = %*{"message": "エラーが発生しました"}
      resp Http400, headers, $response

  get "/@id":
    resp @"id"

proc main() =
  let settings = newSettings(port=Port(8000))
  var jester = initJester(route, settings=settings)
  jester.serve()

main()

jesterではrequestという変数がルーティングのスコープの中で使えるようになります。ここからリクエストが持つ様々な値を取り出すことができます。

prologue

https://github.com/planety/prologue
https://planety.github.io/prologue/

prologueは非同期処理に対応したフルスタックウェブフレームワークです。jesterの次に人気のあるウェブフレームワークのようです。公式ドキュメントが綺麗ですね!
ドキュメントでは言及されていませんが、私にはDjangoへの設計の近さを感じました。
テンプレートエンジンにはKaraxを使うことが推奨されているようです
NimのSPAフレームワーク "Karax" のご紹介

app.nim
import prologue
import ./urls

let settings = newSettings(port=Port(8000))
var app = newApp(settings = settings)
app.addRoute(urls.urlPatterns, "")
app.run()
urls.nim
import prologue

import ./views

const urlPatterns* = @[
  pattern("/", index, HttpGet),
  pattern("/", store, HttpPost),
  pattern("/{id}", show)
]
views.nim
import json
import prologue
import ./templates.index_view

proc index*(ctx: Context) {.async.} =
  resp index_view.render()

proc store*(ctx: Context) {.async.} =
  try:
    let params = ctx.request.body.parseJson
    let name = params["name"].getStr
    let response = %*{"message": "Hello " & name}
    resp jsonResponse(response)
  except Exception:
    let response = %*{"message": "エラーが発生しました"}
    resp jsonResponse(response, Http400)

proc show*(ctx: Context) {.async.} =
  let id = ctx.getPathParams("id")
  resp id
templates/index_view.nim
import karax / [karaxdsl, vdom]

proc render*(): string =
  let vnode = buildHtml(tdiv(class = "mt-3")):
    h1: text("Hello World")
  result = $vnode

basolato

basolatoはasynchttpserverをベースにしたMVCのフルスタックウェブフレームワークです。PHPのLaravel4と同じような使い勝手を目指しつつ、DDDを取り入れた開発にレールを敷いて、フレームワークがビジネスロジックに介在しないように設計されています。
ActiveRecordはありませんが、代わりにallographerというクエリビルダがデフォルトで使えるようになっています。
LaravelそっくりなNim製クエリビルダallographerを作った話

ducereというCLIツールも機能が豊富で、最適化されたビルドやコントローラーやドメインモデル、ビューの作成がコマンド一発で行えます。

ビューはいわるゆるMPAのテンプレートエンジンですが、NuxtJSの設計を参考にCssInJsに影響を受けた機能もあり、コンポーネント=htmlとCSSの塊を返す関数で作ることができます。
このため初期の開発ではMPAで作り、プロダクトがグロースした後はビューのコンポーネントをそのままReact/Vueに置き換え、更にコントローラーとビジネスロジックが分離しているため、コントローラーのわずかな変更でビジネスロジックに手を加えることなくAPIサーバーに置き換えることができます。

# プロジェクトを作成
ducere new basolato_app
cd basolato_app

# コントローラーを作成
ducere make controller hoge
>Created controller hoge_controller.nim

# ビューを作成
ducere make page hello_world
>created page view /basolato_app/resources/pages/hello_world_view.nim

# アプリケーションを起動
ducere serve -p 8000
>Starting 1 thread
>Listening on port 8000
main.nim
# framework
import basolato
# controller
import app/controllers/hoge_controller

var routes = newRoutes()
routes.get("/", hoge_controller.index)
routes.post("/", hoge_controller.store)
routes.get("/{id:int}", hoge_controller.show)

serve(routes)
app/controllers/hoge_controller.nim
import json
# framework
import basolato/controller
# view
import ../../resources/pages/hello_world_view

proc index*(request:Request, params:Params):Future[Response] {.async.} =
  return render(helloWorldView())

proc store*(request:Request, params:Params):Future[Response] {.async.} =
  try:
    let name = params.getJson["name"].getStr
    let response = %*{"message": "Hello " & name}
    return render(response)
  except Exception:
    let response = %*{"message": "エラーが発生しました"}
    return render(Http400, response)

proc show*(request:Request, params:Params):Future[Response] {.async.} =
  let id = params.getInt("id")
  return render($id)
resources/pages/hello_world_view.nim
import basolato/view
import ../layouts/application_view

proc impl():string = tmpli html"""
<h1>Hello World</h1>
"""

proc helloWorldView*():string =
  let title = ""
  return applicationView(title, impl())

phoon

phoonはExpressJSに影響を受けたマイクロフレームワークです。
私の手元ではフレームワーク内部のバグにより、動かすことができませんでした。開発も停滞しているので今後に期待です。

/root/.nimble/pkgs/phoon-0.1.0/phoon.nim(13, 32) Error: ambiguous identifier: 'Callback' -- use one of the following:
  asyncdispatch.Callback: Callback
  route.Callback: Callback

akane

akaneは内部的に非同期処理を実装したマイクロフレームワークのようです。
まだまだドキュメントの不足や、開発の滞りが見られるので今後に期待です。

import akane, json, re

proc main =
  var server = newServer("0.0.0.0", 8000)

  server.pages:
    equals("/", HttpGet):
      await request.answer("<h1>Hello World</h1>")

    equals("/", HttpPost):
      try:
        let params = request.body.parseJson
        let name = params["name"].getStr
        let response = %*{"message": "Hello " & name}
        await request.sendJson(response)
      except Exception:
        let response = %*{"message": "エラーが発生しました"}
        await request.sendJson(response, Http400)

    regex(re"\/\d", HttpGet):
      let id = decoded_url.split("/")[1]
      await request.answer(id)

  server.start()
main()

rosencrantz

rosencrantzはasynchttpserverをベースにしてScalaのAkkaというフレームワークを参考にして作られたウェブ開発用のDSLです。

DSLが難解すぎて私には実装できませんでした…

whip

whipはhttpbeastをベースにしたマイクロフレームワークです。めちゃくちゃ速いようです。
JsonNode型が標準ライブラリにあるものではなく、packedjsonというライブラリに入ってるものを使わなければいけないようで、そこは注意が必要です。

import whip, sugar, packedjson

proc store(params:JsonNode):JsonNode =
  try:
    let name = params["name"].getStr
    let response = %*{"message": "Hello " & name}
    return response
  except Exception:
    let response = %*{"message": "エラーが発生しました"}
    return response

let w = initWhip()
w.onGet "/", (r:Wreq) => r.html("<h1>Hello World</h1>")
w.onPost "/", (r:Wreq) => r.json(store(r.body))
w.onGet "/{id}", (r:Wreq) => r.html(r.path("id"))

w.start(8000)

servy

https://github.com/xmonader/nim-servy
https://xmonader.github.io/nim-servy/

servyは軽量なマイクロフレームワークとのことです。
こちらはnimbleでインストールしてもservy.nimのファイルがなく、ビルド済みバイナリがライブラリの中にできてしまい、フレームワークを使うことができませんでした。

geminim

geminimはNimによるgemini protocolの実装です。
gemini protocolというのは、これまでよりもより低電力消費で、低速ネットワークでも稼働し、プライバシーを非常に重視した新しいインターネットプロトコルとのことです。
非常に低レイヤーでの実装であり、私の力量ではサンプルを実装することができませんでした。

まとめ

いかがでしたでしょうか。
ウェブフレームワークは玉石混交ではありますが、2020年になってNimでもようやくプロダクションユースで使えるウェブフレームワークが出てきたと思います。
私が開発しているbasolatoも今後も頑張って開発していくので、ぜひ2021年からのウェブ開発ではNimを使ってみることを検討してもらいたいと思います!

41
19
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
41
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?