Introduction

Okapi is a microframework for building web servers in Haskell. In contrast to other web frameworks in the Haskell ecosystem, Okapi is primarily concerned with being easy to understand and use, instead of extreme type safety.

Example Server

Here's an example of a simple web server:

{-# LANGUAGE OverloadedStrings #-}
import Data.Text
import Okapi
main :: IO ()
main = runOkapi id 3000 greet
greet = do
seg "greet"
name <- segParam
okPlainText [] $ "Hello " <> name <> "! I'm Okapi."

Running this code will start a server on localhost:3000. If you go to http://localhost:3000/greeting/Bob the server will respond with Hello Bob! I'm Okapi. in plain text format.

Okapi is a monadic parser for HTTP requests that returns HTTP responses as results. This means it can be used with all Applicative, Alternative, and Monad typeclass methods, plus other Haskell idioms like parser combinators.

Here's a more complicated example that implements an API for a calculator. Type annotations are added to make the code easier to follow:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Applicative ((<|>))
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi
main :: IO ()
main = runOkapi id 3000 calc
type Okapi a = OkapiT IO a
calc :: Okapi Result
calc = do
get
seg "calc"
addOp <|> subOp <|> mulOp <|> divOp
addOp :: Okapi Result
addOp = do
seg "add"
(x, y) <- getArgs
okJSON [] $ x + y
subOp :: Okapi Result
subOp = do
seg "sub" <|> seg "minus"
(x, y) <- getArgs
okJSON [] $ x - y
mulOp :: Okapi Result
mulOp = do
seg "mul"
(x, y) <- getArgs
okJSON [] $ x * y
data DivResult = DivResult
{ answer :: Int,
remainder :: Int
}
deriving (Eq, Show, Generic, ToJSON)
divOp :: Okapi Result
divOp = do
seg "div"
(x, y) <- getArgs
if y == 0
then error403 [] "Forbidden"
else okJSON [] $ DivResult {answer = x `div` y, remainder = x `mod` y}
getArgs :: Okapi (Int, Int)
getArgs = getArgsFromPath <|> getArgsFromQueryParams
where
getArgsFromPath :: Okapi (Int, Int)
getArgsFromPath = do
x <- segParam
y <- segParam
pure (x, y)
getArgsFromQueryParams :: Okapi (Int, Int)
getArgsFromQueryParams = do
x <- queryParam "x"
y <- queryParam "y"
pure (x, y)

Okapi is very flexible. You could also define the above without do notation or type annotations:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Applicative ((<|>))
import Control.Monad.Combinators (choice)
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi
main :: IO ()
main = runOkapi id 3000 calcNoDo
calcNoDo = get >> seg "calc" >> choice [addOp, subOp, mulOp, divOp]
addOp = seg "add" >> (getArgs >>= (\(x, y) -> okJSON [] $ x + y))
subOp = (seg "sub" <|> seg "minus") >> (getArgs >>= (\(x, y) -> okJSON [] $ x - y))
mulOp = seg "mul" >> (getArgs >>= (\(x, y) -> okJSON [] $ x * y))
data DivResult = DivResult
{ answer :: Int,
remainder :: Int
}
deriving (Eq, Show, Generic, ToJSON)
divOp =
seg "div"
>> ( getArgs
>>= ( \(x, y) ->
if y == 0
then error403 [] "Forbidden"
else okJSON [] $ DivResult (x `div` y) (x `mod` y)
)
)
getArgs = getArgsFromPath <|> getArgsFromQueryParams
where
getArgsFromPath = segParam >>= (\x -> segParam >>= (\y -> pure (x, y)))
getArgsFromQueryParams = queryParam "x" >>= (\x -> queryParam "y" >>= (\y -> pure (x, y)))

You can also define the same API as a composition of monad comprehensions:

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Applicative ((<|>))
import Data.Aeson (ToJSON)
import Data.Text
import GHC.Generics (Generic)
import Okapi
main :: IO ()
main = runOkapi id 3000 calc
type Okapi a = OkapiT IO a
calc = [result | _ <- get, _ <- seg "calc", result <- addOp <|> subOp <|> mulOp <|> divOp]
addOp = [x + y | _ <- seg "add", (x, y) <- getArgs] >>= okJSON []
subOp = [x - y | _ <- seg "sub" <|> seg "minus", (x, y) <- getArgs] >>= okJSON []
mulOp = [x * y | _ <- seg "mul", (x, y) <- getArgs] >>= okJSON []
data DivResult = DivResult
{ answer :: Int,
remainder :: Int
}
deriving (Eq, Show, Generic, ToJSON)
divOp =
[(x, y) | _ <- seg "div", (x, y) <- getArgs]
>>= ( \(x, y) ->
if y == 0
then error403 [] "Forbidden"
else okJSON [] $ DivResult {answer = x `div` y, remainder = x `mod` y}
)
getArgs =
[(x, y) | x <- segParam, y <- segParam]
<|> [(x, y) | x <- queryParam "x", y <- queryParam "y"]

As you can see, Okapi's parsing functions are very modular and can be easily composed with one another to create parsing primitives tailored specifically to your needs.

With Okapi, and the rest of the amazing Haskell ecosystem, you can create anything from simple website servers to complex APIs for web apps. All you need to get started is basic knowledge about the structure of HTTP requests and an idea of how monadic parsing works.

Okapi IS

  • Still in the early development phase
  • A thin abstraction built on top of WAI: Haskell's web application interface
  • A microframework that's unopinionated and works with your favorite Haskell libraries
  • Easy to compose with other WAI-based web frameworks like Yesod and Servant
  • Easy to learn and understand
  • Great for those new to Haskell
  • Great for rapid prototyping

Okapi IS NOT

  • Production ready
  • A web framework with all the bells and whistles like Yesod, Django, or Rails
  • A web framework that can generate clients or OpenAPI documentation like Servant
  • A web framework that uses advanced Haskell features like type-level programming

💡

If you're convinced that this a good idea, I need help! Feel free to contribute on GitHub however you can:

  • If you're new to Haskell: You can help with documentation and making Okapi easy to use for others by sharing your experience with other web frameworks in any language.
  • If you're an intermediate Haskeller: What have been pain points you've encountered when using other Haskell web frameworks? I need help with testing Okapi and making the API as nice as possible for users like you.
  • If you're an advanced Haskeller: In what ways can performance be improved? How can we make Okapi more compatible with other libraries in the ecosystem? Any addtional tips or concerns will go a long way.

If you're still not convinced, checkout the motivation behind this project.