カテゴリー:Web
インストール方法:nimble install prologue
対応Nimバージョン:2.0.0以降
公式レポジトリ:planety/prologue
Prologueとは?
概要
Prologue は、Nim で HTTP サーバーを組み立てるときに必要になりやすい要素を、最初からある程度まとまった形で提供するフレームワークである。
単にリクエストを受け取って文字列を返すだけではなく、設定管理、ルーティング、ミドルウェア、Cookie、静的ファイル配信、エラーハンドラ、セッション、バリデーション、WebSocket、OpenAPI ドキュメント公開の入口まで揃っているため、
学習用の小さなアプリから、ある程度まとまった業務用ツールや API サーバーまで育てやすい。
Prologue の中心は Context である。各リクエストが入るたびに Context が初期化され、その中に request、response、session などが入る。
ハンドラは proc hello(ctx: Context) {.async.} のような形で書き、ctx.response でレスポンスを返す。
この構造は初心者にとっても理解しやすく、どこに入力があり、どこに出力を書けばよいかが見えやすい。
さらに、newApp() でアプリケーション本体を作り、addRoute や get などでルートを登録し、最後に run() で起動する流れがはっきりしている。
小さく始めたあとで、設定ファイルの導入、ルートのグループ化、ミドルウェア追加、セッション導入といった拡張を段階的に足せる点が、Prologue の扱いやすさにつながっている。
インストール方法
まず前提として、Nim のパッケージ導入には nimble を使う。
Nimble は Nim の標準パッケージマネージャで、通常は Nim と一緒に入る。したがって、Prologue を使う前に Nim 本体が入っていれば、追加の準備はかなり少ない。
Prologue のインストールは、コマンドプロンプトで nimble install prologue と打つだけで完了する。
これで Prologue とその依存関係が自動的にダウンロードされ、ビルドされる。
インストール後は、Nim のコード内で import prologue と書くだけで、Prologue の機能を使えるようになる。
CLI ツールの logue を使う場合は、~/.nimble/bin が環境変数 PATH に入っている必要がある。
ここが通っていないと、インストール自体は終わっていても logue コマンドが見つからない。
Chronos 系バックエンドを試したい場合は、公式の package 定義にある feature を使って次のように導入できる。
通常の学習段階では必須ではないが、将来的にバックエンド差し替えを意識するなら知っておいてよい。
nimble install prologue[kairos]
これで、Prologue のバックエンドがデフォルトの asyncdispatch から kairos に切り替わる。
なお、バックエンドを切り替えるときは、プロジェクトの nim.cfg に prologue:backend = kairos と書いておくと、ビルドのたびに正しいバックエンドが選ばれるようになる。
導入後、最小構成のアプリは import prologue、newApp()、addRoute()、run() で起動できる。
プロジェクトのひな形を一気に作りたいなら、logue init プロジェクト名 が使える。
logue init myapp
JSON 設定ファイル前提にしたいなら --useConfig もある。
logue init myapp --useConfig
特徴
Prologue の強みは、単なる「ルーティングだけの薄いラッパー」ではなく、Web アプリ開発で後から欲しくなりやすい部品を最初から同じ流儀で扱えるところにある。
公式 README でも、設定、Context、パラメータ・クエリ、フォーム、静的ファイル、ミドルウェア、Cookie、起動・終了イベント、URL 生成、エラーハンドラに加えて、I18n、Basic Authentication、最小限の OpenAPI、WebSocket、CORS、Validation、Session、Cache、CSRF、Clickjacking Protection などが列挙されている。
つまり、学習を始めた直後は簡単な Web サーバーとして使え、慣れてきたらそのまま本格機能へ寄せやすい。
ルーティング機能が豊富なのも大きい。
単純な固定パスだけでなく、/hello/{name} のようなパラメータ付きルート、* や $ を使ったワイルドカードや greedy マッチ、正規表現ルート、pattern() を使ったパターン定義、newGroup() を使ったグループルーティングまで扱える。
API を /api/v1/... のように整理したいときにも向いている。
また、Context を中心に設計されているため、クエリ文字列、POST データ、フォームデータ、アップロードファイル、セッション、レスポンス操作が同じ感覚で触れる。
getQueryParams、getPathParams、getFormParams、getUploadFile などの名前がかなり素直で、初心者でも「どこから何を取るのか」を追いやすい。
一方で、テンプレートエンジンはフレームワーク本体に内蔵していない。これは弱点にも見えるが、見方を変えれば役割分担が明確ということでもある。
公式ドキュメントでは Karax を推奨しており、サーバーサイドレンダリングにも向くと案内している。
そのため、Prologue は「バックエンドの骨組みはしっかり持ちたいが、ビュー層は自分で選びたい」という人に合う。
使いどころ
まず向いているのは、学習用の小さな Web アプリである。
Hello World の段階では newApp()、addRoute()、run() だけで始められるので、Nim で Web サーバーがどう動くかを掴みやすい。
それでいて、あとからフォーム処理や静的ファイル配信を足せるため、単発の練習で終わりにくい。
次に向いているのは、Web API や管理画面のような「画面はそこまで派手でなくてよいが、ルーティングや入力処理はきちんとしたい」ケースである。
Prologue にはルートグループ、クエリ・パス・フォーム取得、バリデーション、最小限の OpenAPI ドキュメント公開があるので、JSON API や社内ツールの基盤としてまとめやすい。
ログイン状態を持つ小規模アプリにも向く。
セッション機能があり、sessionMiddleware を通せばハンドラから ctx.session を扱える。
簡易な会員機能、管理者ログイン、設定保存のような処理を実装しやすい。
ただし、公式ドキュメントでも signed cookie ベースのセッションは機密情報保存に安全ではないと明記されているので、そこは設計判断が必要である。
ファイルアップロードや簡単なリアルタイム処理が必要なときにも候補になる。
getUploadFile と save でアップロードを受け取り、WebSocket サポートで簡単な echo サーバーも書ける。
チャット、通知、進捗表示のような機能を Nim でまとめたい場合には相性がよい。
使い方
最初は最小構成を一度動かすのがよい。
公式ドキュメントの基本形は、Context を受け取る非同期ハンドラを書き、resp で本文を返し、newApp() と addRoute() で登録して run() する形である。
これが Prologue の土台になる。
# app.nim
import prologue
proc hello(ctx: Context) {.async.} =
resp "<h1>Hello, Prologue!</h1>"
var app = newApp()
app.addRoute("/", hello)
app.run()
これを nim r app.nim で動かすと、http://localhost:8080/ にアクセスしたときに Hello, Prologue! と表示される。
まずはこの流れを掴むことが重要である。
ここから一歩進めて、クエリ文字列、パスパラメータ、フォーム、セッション、静的ファイルをまとめて触るサンプルを書くと、Prologue の全体像がかなり見える。
公式ドキュメントでは getQueryParams、getPathParams、getFormParams、sessionMiddleware、staticFileMiddleware、redirect などがそれぞれ案内されているので、それを初心者向けに一つへまとめると次のようになる。
import prologue
import prologue/middlewares/staticfile
import prologue/middlewares/sessions/signedcookiesession
proc index(ctx: Context) {.async.} =
let name = ctx.getQueryParams("name", "Guest")
resp "<h1>Hello, " & name & "</h1><p><a href='/login'>login</a></p>"
proc userPage(ctx: Context) {.async.} =
let id = ctx.getPathParams("id", 0)
resp "<h1>User ID: " & $id & "</h1>"
proc login(ctx: Context) {.async.} =
if ctx.request.reqMethod == HttpGet:
resp """
<h1>Login</h1>
<form method="post" action="/login">
<input type="text" name="name" placeholder="your name">
<button type="submit">Login</button>
</form>
"""
else:
let name = ctx.getFormParams("name", "Anonymous")
ctx.session["name"] = name
resp redirect("/mypage")
proc mypage(ctx: Context) {.async.} =
let name = ctx.session.getOrDefault("name", "未ログイン")
resp "<h1>My Page</h1><p>name = " & name & "</p>"
let settings = newSettings(
appName = "PrologueSample",
port = Port(8080),
debug = true
)
var app = newApp(settings = settings)
app.use(staticFileMiddleware("public"))
app.use(sessionMiddleware(settings))
app.get("/", index, name = "index")
app.get("/users/{id}", userPage)
app.all("/login", login)
app.get("/mypage", mypage)
app.run()
このサンプルは、ひとことで言えば「簡単なWebアプリの最小実用版」である。
ただ文字列を返すだけではなく、次の4つをまとめて扱っている。
- ひとつ目は、URL の ?name=Alice のようなクエリ文字列を読むこと。
- ふたつ目は、/users/10 のようなパスの一部を値として受け取ること。
- みっつ目は、フォームから送信された名前を受け取ること。
- よっつ目は、その名前をセッションに保存して、別ページでも使えるようにすること。
各 proc は「ページ1枚分の処理」だと思えばよい
index の役割
index は、クエリ文字列から name を取ってきて、Hello, name と返す処理である。
つまり、URL に ?name=Alice とつけてアクセスすると、Hello, Alice と表示される。
クエリ文字列がないときは Guest になるようにしている。
userPage の役割
userPage は、/users/10 のような URL から id を取ってきて、User ID: 10 と返す処理である。
つまり、URL に /users/10 とアクセスすると、User ID: 10 と表示される。
パスパラメータがないときは 0 になるようにしている。
login の役割
login は、GET と POST の両方を受け取る処理である。
GET のときはログインフォームを返し、POST のときはフォームから name を取ってきてセッションに保存し、/mypage にリダイレクトする。
つまり、/login にアクセスしてフォームに名前を入れて送信すると、その名前がセッションに保存されて /mypage に飛ぶ。
mypage の役割
mypage は、セッションから name を取ってきて、My Page と一緒に表示する処理である。
つまり、/mypage にアクセスすると、My Page と表示され、その下に name = (保存された名前) と出る。
もしセッションに名前が保存されていないときは、name = 未ログイン と表示される。
settings の役割
settings は、アプリ全体の設定をまとめるためのものである。
ここでは、アプリ名、ポート番号、デバッグモードのオンオフを設定している。
これを newApp() に渡すことで、アプリ全体でこの設定が使えるようになる。
app を作る部分
ここでは、まず newApp() でアプリケーション本体を作り、use() でミドルウェアを追加し、get() や all() でルートを登録している。
最後に run() でサーバーを起動する。
つまり、ここでは「どんな機能を使うか」「どんなURLがあるか」をまとめている。
ミドルウェアを追加する部分
Prologue では、追加機能をミドルウェアとして app に組み込むことが多い。
ここでは、静的ファイル配信とセッション管理のミドルウェアを追加している。
staticFileMiddleware("public") は、public ディレクトリにあるファイルを静的に配信するミドルウェアである。
これを追加すると、/style.css のような URL で public/style.css をアクセスできるようになる。
つまり、静的ファイルを置く場所と URL の対応を作る役割がある。
sessionMiddleware(settings) は、セッション管理のミドルウェアである。
これを追加すると、ハンドラの中で ctx.session を使ってセッションデータを読み書きできるようになる。
つまり、ユーザーごとの状態を保存するための機能を提供する役割がある。
ルート登録
ここでは、どの URL にアクセスされたら、どの proc を呼ぶかを登録している。
app.get("/", index, name = "index") は、/ にアクセスされたときに index を呼ぶルートを登録している。
つまり、トップページにアクセスされたときの処理を定義している。
app.get("/users/{id}", userPage) は、/users/10 のような URL にアクセスされたときに userPage を呼ぶルートを登録している。
つまり、ユーザーページにアクセスされたときの処理を定義している。
app.all("/login", login) は、all を使っているので、GET でも POST でも同じ login proc が呼ばれる。
そして /login にアクセスされたときに login を呼ぶルートを登録している。
つまり、ログインページにアクセスされたときの処理を定義している。
この書き方の利点は、/login に関する表示と送信処理を1か所にまとめられることだ。
初心者には少し見やすい。
ただし、もっと大きなアプリでは GET 用 proc と POST 用 proc を分けることも多い。
app.get("/mypage", mypage) は、/mypage にアクセスされたときに mypage を呼ぶルートを登録している。
つまり、マイページにアクセスされたときの処理を定義している。
実行するとどう動くか
まず http://localhost:8080/ にアクセスすると、index が呼ばれる。
ここではクエリ文字列がなければ Guest が表示される。
http://localhost:8080/?name=Alice なら Hello, Alice になる。
次に、ページ内の login リンクを押して /login へ行く。
これは GET なので、login proc の前半が動き、フォーム画面が表示される。
そこで名前を入力して送信すると、今度は POST /login になる。
すると login proc の後半が動き、ctx.getFormParams("name", "Anonymous") で入力値を取り出し、それを ctx.session["name"] に保存する。
そのあと /mypage へリダイレクトされる。
つまり、セッションに名前が保存された状態で /mypage にアクセスすることになる。
/mypage では mypage proc が動き、セッションから "name" を読んで表示する。 そのため、さっき入力した名前が見える。
この一連の流れで、Prologue の基本である「ルート」「フォーム」「セッション」「レスポンス」が全部つながる。
静的ファイルを使いたい場合は、たとえば public/style.css を置いて、HTML 側で /style.css のように絶対パスで読む。
公式ドキュメントでは静的ファイルはミドルウェアで配信し、パスはバイナリ位置からの相対になると説明されている。
開発中はこれで十分だが、本番ではリバースプロキシ側に任せる設計が勧められている。
設定をコード直書きではなく外へ出したいなら、newSettings(...) に直接値を書く方法のほか、.env を読む方法や JSON 設定ファイルを使う方法がある。
大きくなる予定のアプリなら、早めに設定ファイル化したほうが整理しやすい。
newAppQueryEnv() を使うと、PROLOGUE 環境変数に応じて .config/config.json や .config/config.production.json のようなファイルを自動で読み分けられる。
もしルーティングが増えてきたら、newGroup() でグループ化するとよい。
/api/v1 のような共通接頭辞を持つ API をまとめられるため、管理しやすくなる。
URL を名前から逆引きしたいときは urlFor() も使える。
画面遷移やリンク生成が増えてきた段階で特に有効である。
HTML をもっと見通しよく書きたい場合、Prologue 本体にテンプレートエンジンはない。
その代わり、公式ドキュメントでは Karax を推奨している。
最初は文字列で十分だが、ページ数が増えたら Karax で部品化すると保守しやすくなる。
注意点
このコードで詰まりやすいところも補足しておく。
最初に気をつけるべきなのは、logue コマンドが見つからない問題である。
これは Prologue 自体の不具合というより、~/.nimble/bin が PATH に入っていないことが原因になりやすい。
インストール後に logue init ... が動かないなら、まずそこを確認する。
設定ファイルを使う場合は、secretKey が空だと例外になる。
公式の設定ドキュメントでも secretKey は必須で、空にしてはいけないとされている。
Core API には EmptySecretKeyError も定義されている。
したがって、JSON 設定や .env を使うなら、開発中でも適当な固定値を必ず入れておくべきである。
フォーム処理では、getPostParamsOption と getFormParamsOption を混同しやすい。
公式ドキュメントでは getPostParamsOption は application/x-www-form-urlencoded だけを対象にし、getFormParams は form-urlencoded と multipart/form-data の両方を扱えると説明されている。
つまり、ファイルアップロードを含むフォームなのに getPostParams 系を使うと、期待通りに取れないことがある。
アップロードが絡むなら getFormParams と getUploadFile を使うほうが安全である。
ルーティングでは RouteError と重複ルートの問題に注意が必要である。
公式ドキュメントでは greedy 記号 $ は URL の末尾でしか使えず、途中に置くと RouteError になる。
また Core API には DuplicatedRouteError も定義されている。
ルートが増えてきたら、似たパスを何本も無造作に生やさず、グループ化や命名を早めに行うほうがよい。
静的ファイルは「動く環境」と「配置場所」の食い違いで詰まりやすい。
公式ミドルウェア解説では、静的ファイル用のパスはバイナリ位置からの相対で解釈されるとされている。
つまり、ローカルでは見えるのに、実行ファイルの置き場所が変わった本番環境では 404 になることがある。
また、HTML 内の JS や CSS はフルパスを使うよう FAQ でも案内されている。
見つからないときは、まず配置パスと参照パスを疑うべきである。
セッションも注意が必要である。
公式セッション解説では、signed cookie ベースのセッションは安全ではなく、テスト用途以外で機密情報を保存してはいけないと明記されている。
ユーザー ID 程度の軽い識別情報ならともかく、認証トークンや重要データをそのまま詰める設計は避けるべきである。
本番運用では debug = true のままにしないほうがよい。
QuickStart と FAQ の両方で、debug はログや例外表示に役立つ一方、遅くなり、リリースやベンチマーク時には false にすべきと案内されている。
特に 500 系エラー時、debug 有効だと例外情報がブラウザへ見えることがあるため、公開環境では切るのが基本である。
以上が Prologue を使うときに初心者が詰まりやすいポイントである。
これらを知っておくと、学習の途中で「なぜ動かないのか?」と悩む時間が減るはずである。
型・関数・API
ここでは、Prologue の主要な型や関数、API をまとめて紹介する。
公式ドキュメントの API リファレンスを初心者向けに整理したものである。
初心者が最初に覚えるべき主要 API は、Context を中心に、その周辺の Request、Response、Settings、Session、ルーティング系、起動系である。
以下は公式 Core API Index と各解説ページをもとに、最初の学習で優先度が高いものへ絞った一覧である。
主要な型
- Context:1リクエスト分の状態をまとめて持つ中心型で、request・response・session などへアクセスする入口になる。
- Request:受信したHTTPリクエスト本体を表す型で、URL、パス、メソッド、ヘッダ、Cookieなどを読むときに使う。
- Response:返すHTTPレスポンスを表す型で、本文やステータスコード、ヘッダを組み立てるときに使う。
- ResponseHeaders:レスポンスヘッダを保持する型で、Content-Type や Cookie などを制御するときに使う。
- Settings:ポート、debug、secretKey などアプリ設定をまとめる型で、newSettings や loadSettings の戻り値になる。
- Prologue:アプリケーション本体を表す型で、ルート登録、ミドルウェア追加、起動の中心になる。
- Session:セッションデータを表す型で、ログイン状態や一時的なユーザー状態を保存するときに使う。
- UploadFile:アップロードされたファイルを表す型で、save してディスクへ保存できる。
- Group:ルートのグループを表す型で、/api や /admin のような共通プレフィックスをまとめるときに使う。
- Router:内部ルーティング構造を表す型で、複数ルートの登録や確認の土台になる。
- HandlerAsync:非同期ハンドラの型で、ルート処理やミドルウェアに渡す手続きの型として使われる。
主要な関数・マクロ
newApp():Prologueアプリ本体を作るための開始点である。newSettings():ポートや debug、secretKey などの設定を作る。loadSettings():JSON などの設定ファイルから Settings を読み込む。newAppQueryEnv():環境変数に応じて設定ファイルを切り替えてアプリを作る。addRoute():文字列ルート、正規表現ルート、複数HTTPメソッド対応のルートを登録する。get()/post()/all():用途別にルートを簡潔に登録する。newGroup():ルートグループを作り、階層的なURL設計をしやすくする。pattern():パターン定義を先にまとめてからルート登録したいときに使う。use():アプリにミドルウェアを追加する。run():サーバーを通常起動する。runAsync():非同期でサーバーを起動し、他の処理と並行して動かしたいときに使う。resp:もっともよく使うレスポンス返却マクロで、本文やステータスコードを簡潔に返せる。respDefault:既定のエラーページを返したいときに使う。redirect():別URLへリダイレクトするレスポンスを作る。urlFor():名前付きルートからURLを逆引きして組み立てる。getPathParams():/users/{id}のようなパスパラメータを読む。getQueryParams():URL のクエリ文字列を読む。getFormParams():フォームデータを読む。getUploadFile():multipart/form-data のアップロードファイルを取得する。staticFileResponse():静的ファイルをレスポンスとして返す。registerErrorHandler():404 や 500 などのエラーハンドラを差し替える。serveDocs():OpenAPI 用のopenapi.jsonをもとに docs / redocs を公開する。
上の一覧のうち、serveDocs() は OpenAPI 解説ページで紹介されている機能であり、Core API Index の主要項目と合わせて覚えると理解しやすい。
特に newApp、newSettings、addRoute、use、run、resp、getPathParams、getQueryParams、getFormParams、getUploadFile あたりが最初の重要ポイントになる。
列挙型
Prologue には、設定やルーティング、フラッシュメッセージなどの区別に使う列挙型もいくつか定義されている。
これらは、コードの中で特定の種類を表すために使われる。
ConfigFileExt:設定ファイル形式を表す列挙型で、JSON・TOML・YAML などの区別に使う。PatternMatchingType:ルーティング内部のパターン種別を表す列挙型で、テキスト・パラメータ・ワイルドカードの区別に使う。FlashLevel:フラッシュメッセージの重要度を表す列挙型で、Info・Warning・Error・Fault などの分類に使う。
ConfigFileExt には Core API Index で Json、Toml、Yaml が確認できる。
PatternMatchingType には ptrnText、ptrnParam、ptrnWildcard があり、FlashLevel には Info、Warning、Error、Fault がある。
列挙型まで読み始めると、Prologue が見た目よりかなりきちんと整理されたフレームワークだとわかる。
例外・エラー系で知っておくとよいもの
EmptySecretKeyError:secretKey が空のときに問題になる例外で、設定ファイルまわりの初学者が踏みやすい。EnvError:環境設定の読込や内容に問題があるときの系統である。EnvWrongFormatError:環境設定の形式が期待と合わないときの系統である。DuplicatedRouteError:重複したルート登録が起きたときに関係する例外である。RouteError:ルーティングパターンの書き方が不正なときに関係する例外である。PrologueError:Prologue 全体で使われる基底寄りのエラー系である。
これらは最初から全部覚える必要はないが、設定が読めない、ルートが競合する、パターンがおかしい、といった場面で名前を見ても慌てにくくなる。
参考文献
以下は本記事を執筆するにあたり参考にした文献である。