はじめに
HaskellのWebフレームワークはYesod、Spockなどいくつかありますが、そのなかでも軽量なScottyを紹介します。
RubyのSinatraに影響を受けており、非常に簡単にWebサーバを作ることができます。
準備
以下のライブラリを使いますので、package.yaml
などに追加しておいてください。
- http-types
- aeson
- scotty
まずとても簡単な例
以下のようなコードを書いて実行してみましょう。たった5行でサーバを立てることができます。
{-# LANGUAGE OverloadedStrings #-}
module Sample where
import Web.Scotty
main :: IO ()
main = scotty 3000 $ get "/" $ html "<h1>Hello</h1>"
http://localhost:3000/
にアクセスしてみましょう。
ユーザのAPI
例としてユーザデータを参照/作成/削除をするAPIを実装してみます。簡単のためにDBなどは使わずにオンメモリにデータを保存することにします。
ユーザデータの定義
まず、JSONで扱えるユーザデータを定義します。また、参照/作成/削除の関数も実装しておきます。
data User = User { uid :: Integer, name :: String, age :: Integer } deriving (Generic, Show)
instance ToJSON User
instance FromJSON User
addUser :: [User] -> User -> [User]
addUser users user = user:users
deleteUser :: [User] -> Integer -> [User]
deleteUser users i = filter (\user -> uid user /= i) users
findUser :: [User] -> Integer -> Maybe User
findUser users i = find (\u -> uid u == i) users
エラーの定義
data Error = Error { message :: String } deriving (Generic, Show)
instance ToJSON Error
instance FromJSON Error
ユーザの簡易のリポジトリ
Listでユーザのリポジトリを作成します。今回はListの中身を変更できるようにIORef
を使います。
IORef
は例外的にミュータブルな(=変更可能な)変数を扱うためのモナドです。
readIORef
で[User]
型データを読み込んだり、writeIORef
で書き換えができます。
main = do
users <- newIORef [] :: IO (IORef [User])
Scottyの起動
scotty
関数を呼び出すことで起動します。次の例ではポート3000番でlistenします。
main = do
-- (省略)
scotty 3000 $ do
ユーザの取得
すべてのユーザを取得するAPIを作成します。
liftIO
についてはこちらを参照してみてください。
get "/users" $ do
us <- liftIO (readIORef users) -- リポジトリからすべてのユーザを取得
status status200
json us
$ curl -X GET http://localhost:3000/users
さらにPathパラメータ:uid
を利用して、特定のユーザを取得するAPIも作成します。
get "/users/:uid" $ do
us <- liftIO (readIORef users)
i <- param "uid" -- Pathパラメータ uid を取得
case findUser us (read i) of -- 指定されたユーザが存在するか否かで場合分け
Just u -> status status200 >> json u
Nothing -> status status404 >> json (Error ("Not Found uid = " <> i))
$ curl -X GET http://localhost:3000/users/1
ユーザの作成
ユーザを作成するAPIを作成します。
post "/users" $ do
u <- jsonData -- BodyをJSONで取得
us <- liftIO $ readIORef users
liftIO $ writeIORef users $ addUser us u -- 新規ユーザが追加されたリストにリポジトリを書き換え
status status201
json u
$ curl -X POST http://localhost:3000/users -d '{ "uid": 1, "name": "alice", "age": 20 }'
ユーザの削除
ユーザを削除するAPIを作成します。
delete "/users/:uid" $ do
i <- param "uid"
us <- liftIO $ readIORef users
liftIO $ writeIORef users $ deleteUser us i
status status204
curl -v -X DELETE http://localhost:3000/users/1
完成コード
完成したコードは以下のようになります。Scottyを使うと割と簡単にAPIを記述できました!
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module UserAPI where
import Web.Scotty
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics
import Data.IORef
import Control.Monad.Reader
import Network.HTTP.Types.Status
import Data.List (find)
data User = User { uid :: Integer, name :: String, age :: Integer } deriving (Generic, Show)
instance ToJSON User
instance FromJSON User
data Error = Error { message :: String } deriving (Generic, Show)
instance ToJSON Error
instance FromJSON Error
addUser :: [User] -> User -> [User]
addUser users user = user:users
deleteUser :: [User] -> Integer -> [User]
deleteUser users i = filter (\user -> uid user /= i) users
findUser :: [User] -> Integer -> Maybe User
findUser users i = find (\u -> uid u == i) users
main :: IO ()
main = do
users <- newIORef [] :: IO (IORef [User])
scotty 3000 $ do
get "/users" $ do
us <- liftIO (readIORef users)
status status200
json us
get "/users/:uid" $ do
us <- liftIO (readIORef users)
i <- param "uid"
case findUser us (read i) of
Just u -> status status200 >> json u
Nothing -> status status404 >> json (Error ("Not Found uid = " <> i))
post "/users" $ do
u <- jsonData
us <- liftIO $ readIORef users
liftIO $ writeIORef users $ addUser us u
status status201
json u
delete "/users/:uid" $ do
i <- param "uid"
us <- liftIO $ readIORef users
liftIO $ writeIORef users $ deleteUser us i
status status204