Belajar Haskell - Backend Services: Servant / Scotty
Episode 13 of 23

Belajar Haskell - Backend Services: Servant / Scotty

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Memilih Framework

FrameworkGayaKekuatanCocok untuk
ServantType-level APIKontrak API di level tipe, client & docs otomatisAPI-first, team besar
ScottyRingan, sederhanaSetup instan, mirip Flask/ExpressPrototype, service kecil
YesodFull frameworkTemplating, form, i18n terintegrasiAplikasi web lengkap

Untuk series ini kita fokus ke Servant (fitur andalan Haskell) dan Scotty (pintu masuk cepat).

Scotty: Mulai dalam 5 Menit

Scotty adalah micro-framework yang dirancang agar ringan — hampir se-instannya Flask di Python. Contoh endpoint pertama:

app/Main.hs (Scotty)
{-# 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:

Jalankan server
cabal run

Uji dengan curl:

Uji endpoint
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: API sebagai Tipe

Servant mendefinisikan kontrak API di level tipe — bukan sebagai string atau deklarasi runtime. API digambarkan sebagai tipe:

Definisi API di level tipe
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
 
import Servant
 
type UserAPI =
       "users" :> Get '[JSON] [User]
  :<|> "users" :> Capture "id" Int :> Get '[JSON] User

Baca 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.

Data Model dengan Aeson

Serialisasi JSON memakai Aeson. Contoh tipe User dengan instance JSON otomatis:

Model User dengan Aeson
{-# 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 User

ToJSON 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.

Implementasi Handler

Handler hanyalah monad Handler, yang pada dasarnya ExceptT ServerError IO — jadi bisa memakai semua alat yang kita pelajari:

Handler + server lengkap
{-# 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.

Client dan Dokumentasi Otomatis

Dari tipe API yang sama, kalian mendapat client type-safe:

Client dari tipe yang sama
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.

Yesod: Framework Lengkap

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.

Kesalahan Umum (Common Pitfalls)

  1. Lupa OverloadedStrings — literal "users" di path perlu ekstensi ini karena bertipe Text/string di level tipe, bukan String.
  2. Proxy :: Proxy UserAPI wajib di serve — kalian tidak bisa menghilangkan tipe parameter di level value; Proxy adalah "nilai kosong" untuk membawa tipe.
  3. Handler harus komposisi :<|> yang sama dengan tipe — jumlah komponen server harus sama persis dengan jumlah endpoint.
  4. Tidak menyediakan instance ToJSON — error "no instance for ToJSON" sangat umum; tambahkan deriving Generic + instance kosong.
  5. Memakai String untuk payload besar — pakai Text untuk performa dan keamanan (dibahas di episode 21).

Penutup

Inti yang harus dibawa pulang:

  • Scotty untuk memulai cepat; Servant untuk API production yang type-safe; Yesod untuk aplikasi web lengkap.
  • Servant mendefinisikan API sebagai tipe — server, client, dan dokumentasi berasal dari tipe yang sama.
  • Aeson (ToJSON/FromJSON) menangani JSON dengan deriving Generic sebagai pintu cepat.
  • Handler Servant pada dasarnya ExceptT ServerError IO — semua alat monad dari episode 11 langsung berlaku.
  • Jaga logika pure di 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!

Belajar Haskell - Backend Services: Servant / Scotty | Belajar Haskell