Posted at

HaskellとScottyで簡単にAPIを構築する


はじめに

HaskellのWebフレームワークはYesod、Spockなどいくつかありますが、そのなかでも軽量なScottyを紹介します。

RubyのSinatraに影響を受けており、非常に簡単にWebサーバを作ることができます。


準備

以下のライブラリを使いますので、package.yamlなどに追加しておいてください。

- http-types

- aeson
- scotty


まずとても簡単な例

以下のようなコードを書いて実行してみましょう。たった5行でサーバを立てることができます。


Sample.hs

{-# 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を記述できました!


UserAPI.hs

{-# 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