Membangun backend HTTP di Haskell: mendefinisikan API di level tipe dengan Servant yang menghasilkan dokumentasi dan client secara otomatis, memulai cepat dengan Scotty yang ringan, mengenal Yesod untuk aplikasi besar, serta serialisasi JSON dengan Aeson yang terintegrasi type-safe.

Setelah 12 episode membangun fondasi — sintaks, tipe, typeclass, monad, concurrency — saatnya menutup rangkaian "alat" dan masuk ke "produk": membangun backend HTTP. Inilah alasan praktis banyak orang belajar Haskell di 2026: API yang type-safe dari ujung ke ujung, di mana kesalahan kontrak API tertangkap saat kompilasi, bukan di production.
Mengapa bab ini penting? Karena jenis bug paling mahal di dunia backend adalah kontrak API yang menyimpang: response yang kelebihan/kekurangan field, tipe data yang salah, atau endpoint yang lupa diimplementasikan. Servant mengubah masalah ini dari "diuji saat runtime" menjadi "diperiksa saat kompilasi" — dan inilah yang membuat Haskell unik sebagai bahasa backend.
| Framework | Gaya | Kekuatan | Cocok untuk |
|---|---|---|---|
| Servant | Type-level API | Kontrak API di level tipe, client & docs otomatis | API-first, team besar |
| Scotty | Ringan, sederhana | Setup instan, mirip Flask/Express | Prototype, service kecil |
| Yesod | Full framework | Templating, form, i18n terintegrasi | Aplikasi web lengkap |
Untuk series ini kita fokus ke Servant (fitur andalan Haskell) dan Scotty (pintu masuk cepat).
Scotty adalah micro-framework yang dirancang agar ringan — hampir se-instannya Flask di Python. Contoh endpoint pertama:
{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Data.Text.Lazy (Text)
main :: IO ()
main = scotty 3000 $ do
get "/hello/:nama" $ do
nama <- param "nama"
text ("Halo, " <> nama <> "!")scotty 3000 menjalankan server di port 3000, get mendaftarkan handler untuk GET /hello/:nama, dan param membaca parameter path. Tambahkan dependensi scotty dan text ke file .cabal, lalu:
cabal runUji dengan curl:
curl http://localhost:3000/hello/devnull
# Halo, devnull!Scotty bagus untuk memulai karena tidak ada konsep baru — handler hanyalah aksi IO di dalam monad ActionM. Untuk API produksi yang serius, Servant menawarkan jaminan yang jauh lebih kuat.
Servant mendefinisikan kontrak API di level tipe — bukan sebagai string atau deklarasi runtime. API digambarkan sebagai tipe:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Servant
type UserAPI =
"users" :> Get '[JSON] [User]
:<|> "users" :> Capture "id" Int :> Get '[JSON] UserBaca seperti deskripsi domain:
"users" :> Get '[JSON] [User] — path /users dengan method GET, response JSON berisi list User."users" :> Capture "id" Int :> Get '[JSON] User — /users/:id mengembalikan satu User.Tipe ini adalah satu-satunya sumber kebenaran. Dari tipe yang sama, Servant menghasilkan: server handler, client function, dan dokumentasi — semuanya dijamin konsisten karena berasal dari tipe yang sama.
Serialisasi JSON memakai Aeson. Contoh tipe User dengan instance JSON otomatis:
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson (ToJSON, FromJSON)
import GHC.Generics (Generic)
data User = User
{ userId :: Int
, userName :: String
} deriving (Show, Generic)
instance ToJSON User
instance FromJSON UserToJSON dan FromJSON yang di-derive menghasilkan konversi JSON otomatis: {"userId":1,"userName":"..."}. Kalian bisa menyesuaikan nama field dengan fieldLabelModifier bila butuh nama ala snake_case — API JSON menjadi tetap stabil meski struktur internal berubah.
Handler hanyalah monad Handler, yang pada dasarnya ExceptT ServerError IO — jadi bisa memakai semua alat yang kita pelajari:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Servant
import Data.Aeson (ToJSON)
import GHC.Generics (Generic)
data User = User { userId :: Int, userName :: String }
deriving (Show, Generic)
instance ToJSON User
userDB :: [User]
userDB = [User 1 "devnull", User 2 "arman"]
type UserAPI =
"users" :> Get '[JSON] [User]
:<|> "users" :> Capture "id" Int :> Get '[JSON] (Maybe User)
server :: Server UserAPI
server = listUsers :<|> getUser
where
listUsers = pure userDB
getUser id = pure (findUser id)
findUser :: Int -> Maybe User
findUser i = foldr (\u acc -> if userId u == i then Just u else acc) Nothing userDB
main :: IO ()
main = run 8080 (serve (Proxy :: Proxy UserAPI) server)findUser adalah fungsi pure — logika murni, bebas I/O. listUsers dan getUser membungkusnya dalam Handler. Jika kontrak berubah (misal menambah field), compiler langsung menuntun kalian memperbarui handler, client, dan dokumentasi sekaligus.
Tip
Pola "pure core, IO boundary" dari episode 11 berlanjut di sini: jaga agar userDB, findUser, dan logika lain tetap pure, dan biarkan Handler hanyalah adaptor tipis. Dengan cara ini, semua logika API bisa diuji tanpa HTTP — cukup panggil fungsi pure-nya langsung di test.
Dari tipe API yang sama, kalian mendapat client type-safe:
import Servant.Client
listUsers :: ClientM [User]
getUser :: Int -> ClientM (Maybe User)
listUsers :<|> getUser = client (Proxy :: Proxy UserAPI)Dan dokumentasi OpenAPI otomatis via paket servant-openapi3 — dari satu tipe, kalian mendapat server, client, dan docs yang tidak mungkin tidak sinkron.
Untuk aplikasi web besar (form, session, templating, i18n), Yesod menyediakan semuanya dengan pendekatan type-safe yang konsisten. Kekuatannya: template Haskell yang menghilangkan boilerplate, sistem form dengan validasi otomatis, dan integrasi deep dengan Persistent — ORM yang akan kita kenali di episode 15. Yesod punya kurva belajar paling curam, jadi mulai dari Scotty/Servant dulu adalah langkah yang bijak.
OverloadedStrings — literal "users" di path perlu ekstensi ini karena bertipe Text/string di level tipe, bukan String.Proxy :: Proxy UserAPI wajib di serve — kalian tidak bisa menghilangkan tipe parameter di level value; Proxy adalah "nilai kosong" untuk membawa tipe.:<|> yang sama dengan tipe — jumlah komponen server harus sama persis dengan jumlah endpoint.ToJSON — error "no instance for ToJSON" sangat umum; tambahkan deriving Generic + instance kosong.String untuk payload besar — pakai Text untuk performa dan keamanan (dibahas di episode 21).Inti yang harus dibawa pulang:
ToJSON/FromJSON) menangani JSON dengan deriving Generic sebagai pintu cepat.Servant pada dasarnya ExceptT ServerError IO — semua alat monad dari episode 11 langsung berlaku.where/fungsi, biarkan handler menjadi adaptor tipis.Di episode 14 selanjutnya kita akan membahas data processing & streaming — memproses data besar dengan Conduit dan Streaming library, menghindari list penuh di memori, serta kekuatan Haskell untuk internal DSL seperti parser Megaparsec dan konfigurasi. Sampai jumpa di episode 14!