Help us understand the problem. What is going on with this article?

Haskell・Servant+Persistent/Esqueletoで作る実用WebAPI (1) Servantの基本

More than 1 year has passed since last update.

はじめに

この1年半にわたり、業務の基本システムのバックエンドとして、WebAPIをHaskellで実装してきました。Haskell自体やHaskellでのWebAPIの入門記事のおかげで随分と助けられたのですが、一方で業務での稼働となると、それだけでは足りない要素も多く、ネットに散らばっている情報を探ったり試行錯誤をしたりしながら組み上げてきました。

Haskellというと、高度な使い方の記事も非常に多いですが、業務用システムのWebAPIといった泥臭い話題もあってもいいはず、と思い、これまで積み上げてきたものを一通り書いてみようかと思います(最後までいけるか、若干不安ですが)。

読者の想定

本記事シリーズの想定する読者は下記の通りです

  • stackを使って、何らかのHaskellコードが書ける

OSはMacOSかUnixが無難かと思います。Windowsでもそれなりにできるかと思いますが、使用するモジュールが増えてきて、PCREのインストールが必要となるところで苦労する可能性が高いです。私自身は、「Macで開発、Linux(Cent OS)でテスト&稼働」というスタイルでしたが、同僚がWindowsだったため、何回かトライしましたが、うまくいきませんでした。エディタは何でも構いませんが、ghc-modと連携できるものでないと、ビルドエラー時の対応等で作業効率が落ちます。私はEmacsを使っていますが、VSCode等、使いやすいものも多数あるかと思います。

「コードが書ける」部分については、まずは「IOアクションと純粋関数の使い分けができる」くらいでいいかと思います。Maybe等のデータ型に馴染んでいればさらに良いです。

  • 簡単なRDBがわかる

WebAPIを実装する場合、ほとんどのケースでRDBが必要でしょう。簡単なCRUD操作の理解は必須ですが、簡単でいいので、joinについても理解できていると良いです。

  • WebAPIを実装したことがある

他の言語等で、WebAPIを実装したことがあった方がいいです。なくてもなんとかなるかもですが、やはり「わからないことが2つ以上ある」というのは、理解のハードルが上がってしまいます

システム構成の前提

本記事シリーズでは、RDBはMySQLとします。ただし、他のRDBでも基本は変わらないでしょう。フロントエンドはAngularを使いましたが、もちろん、どのフロントエンドフレームワークでも内容は変わりません。1年以上前に決めたのでAngularにしましたが、今ならElmを選んでいたでしょうね(当時は単純にElm等のフレームワークの存在を知りませんでした)。

Servantの基本

ソースコード

「実用系記事を書きます」とはいったものの、やはり基本を押さえてからの「実践」なので、まずはServantの基本からいきます。いくつか押さえるべきポイントはあるものの、単純なAPIだけの場合は1ファイルだけで構成できます。以下は、全ソースコードです。わかりやすくするため、自分で定義した型や関数には「MyApp」のプレフィックスをつけています。なお、本記事では、まだRDBへのアクセスは含みません(次回以降)。

src/Main.hs
{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators     #-}

module Main where

import           Control.Monad.IO.Class   (liftIO)
import           Network.Wai.Handler.Warp (run)
import           Servant

------------------------------
-- API Handler registration --
------------------------------

type MyAppAPI = "add" :> Capture "n1" Int :> Capture "n2" Int :> Get '[JSON] Int
           :<|> "div" :> Capture "n1" Int :> Capture "n2" Int :> Get '[JSON] Int

myAppServer :: Server MyAppAPI
myAppServer = addHandler
         :<|> divHandler

----------------------------
-- API Server application --
----------------------------

myAppApi :: Proxy MyAppAPI
myAppApi = Proxy

myAppApp :: Application
myAppApp = serve myAppApi myAppServer

------------------------
-- Running API Server --
------------------------

main :: IO ()
main = do
  let myAppPort = 8081
  run myAppPort myAppApp

---------------------------
-- API Handlers (sample) --
---------------------------

addHandler :: Int -> Int -> Handler Int
addHandler n1 n2 = do
  liftIO $ print "add"
  return $ n1 + n2

divHandler :: Int -> Int -> Handler Int
divHandler n1 n2 = do
  liftIO $ print "div"
  case n2 of
    0        -> throwError err400 {errBody = "Error division by Zero"}
    non_zero -> return $ n1 `div` non_zero

WebAPIフレームワークの基本

WebAPIフレームワークには、その種類ごとにいろんな機能がありますが、基本的な機能は主に下記のようになるでしょう

  • ルーティング
    APIリクエストの際に、URLをパースし、予め登録されていた「URLのパターンに対応するハンドラ(API動作の実体)」を動作させます。

  • 入力処理
    APIリクエストに含まれる値(URL中のクエリ文字列、ヘッダ、リクエストボディ)をパースし、ハンドラに提供します。

  • 出力処理
    APIリクエストに対するレスポンスを返します。主にレスポンスコード(正常時は200、Not Foundの場合は404など)やレスポンスボディ(JSONで構成されることが多い)で構成されます。

以上のポイントを中心にサンプルコードの説明をします。

ルーティング、入力処理

------------------------------
-- API Handler registration --
------------------------------

type MyAppAPI = "add" :> Capture "n1" Int :> Capture "n2" Int :> Get '[JSON] Int
           :<|> "div" :> Capture "n1" Int :> Capture "n2" Int :> Get '[JSON] Int

myAppServer :: Server MyAppAPI
myAppServer = addHandler
         :<|> divHandler

type MyAppAPIに、URLを型として列記します。:<|>でURLパターンを列記し、各URL内の:>は、URLパスでの「/」を意味します。myAppServerには、MyAppAPI型に対応するように、ハンドラを列記します。

 ハンドラの型は、MyAppAPIのURLに盛り込まれた入力パラメータと一致しなければいけません(一致しないとビルドエラー)。例えば、1つめのAPIの"add"の例を見ると、入力は「Capture Int」が2つあり、戻り値もIntであるため、addHandlerも「IntとIntを引数にとり、Intを返す」型でなければいけません。実際、addHandlerの型はaddHandler :: Int -> Int -> Handler Intとなっています(ハンドラの実装とHandler型については後述)。

 PUTあるいはPOSTメソッドのような、入力値にリクエストボディがある場合には、それもMyAppAPI型に指定して、ハンドラの引数として受け取ります。具体例については、別の記事で記載します。リクエストヘッダについても同様です。

ハンドラ、出力処理

---------------------------
-- API Handlers (sample) --
---------------------------

addHandler :: Int -> Int -> Handler Int
addHandler n1 n2 = do
  liftIO $ print "add"
  return $ n1 + n2

divHandler :: Int -> Int -> Handler Int
divHandler n1 n2 = do
  liftIO $ print "div"
  case n2 of
    0        -> throwError err400 {errBody = "Error division by Zero"}
    non_zero -> return $ n1 `div` non_zero

例として上げているaddHandler, divHandlerはそれぞれ2つの引数の足し算、割り算をするだけ、です。APIにするほどの処理ではないのですが、あくまで「例」ですので。

さて、ハンドラの型は「引数1の型 -> 引数2の型 -> .. -> 引数Nの型 -> Handler 戻り値の型」となります。Main関数なんかに書くIO処理の型は「引数1の型 -> 引数2の型 -> .. -> 引数Nの型 -> IO 戻り値の型」ですから、「IO」の部分が「Handler」に変わっただけ、ともいえます。

この「Handler」ですが、これはServantで「ExceptT ServantErr IO」と定義されています。そのため「通常の値を返す」以外に「ServantErr例外を投げる」ことができます。

divHandlerの例で書いていますが、通常の値を返す場合にはreturnで値を返します。この場合WebAPIとしては、レスポンスコードを200、レスポンスボディをreturnで指定した値、となります。n2が0の場合は、割り算が実行できないため、APIとしてはエラーを返したくなります。このような場合、throwErrorでServantErrを投げると、投げられたServantErrに対応したレスポンスが返ります。例ではerr400を投げていますので、APIのレスポンスとしてレスポンスコード400が返ります。

ExceptT等がついていますが、IOでもあるので、liftIOをつければハンドラ内でIO処理も可能です。例えば、現在時刻を取得したり、printデバッグをしたり、も可能です。ソースコードにはprintを例として入れています。

API実行

----------------------------
-- API Server application --
----------------------------

myAppApi :: Proxy MyAppAPI
myAppApi = Proxy

myAppApp :: Application
myAppApp = serve myAppApi myAppServer

上記で用意した、URLの型(MyAppAPI)とハンドラ登録(myAppServer)を使って、WebAPIアプリケーションを作成し、それを実行します。

Servantのserve関数を使って、WebAPIアプリケーションを作成します。引数にmyAppApiという関数がありますが、これはURLの型(MyAppAPI)をServant側で利用できるようにするために必要、というもののようです(まだ、うまく説明できるレベルではないです)。

------------------------
-- Running API Server --
------------------------

main :: IO ()
main = do
  let myAppPort = 8081
  run myAppPort myAppApp

WebAPIアプリケーションができたので、あとはそれを実行するだけです。Servantではなく、Warp(Webサーバ)の関数として「run」がありますので、これにポート番号と先のアプリケーション(myAppApp)を指定すればOKです。

まとめ

WebAPIアプリケーション作成の部分だけ、若干「おまじない」感がありますが、他の箇所は比較的「必要なものを淡々と並べるだけ」というところかと思います。ハンドラの型には、ハンドラで処理するために必要な引数が全て含まれますので、基本ができれば、あとはハンドラの実装に注力すればよい、となります。

とはいえ、まだこの段階では、ハンドラ実装に必要な機能がまだ足りない状況ですので、それは次回以降の記事で記載します。

今回の記事のリポジトリも用意していますので参考にしてください。
https://github.com/cyclone-t/servant-sample

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away