When working with form data, we often want to serialize and deserialize forms as custom data types, instead of working with the key-value pairs directly. The ToForm and FromForm type classes abstracts serialization and deserialization to form data, respectively.

We first declare our data types, and some instance which we will need later.

data MealType = Vegan | Vegetarian | Omnivore | Carnivore

derive instance genericMealType :: Generic MealType _
instance eqMealType :: Eq MealType where eq = genericEq
instance showMealType :: Show MealType where show = genericShow

newtype Order = Order { beers :: Int, meal :: MealType }

In this example we will only deserialize forms, and thus we only need the FromForm instance.

instance fromFormOrder :: FromForm Order where
  fromForm form = do
    beers <- required "beers" form >>= parseBeers
    meal <- required "meal" form >>= parseMealType
    pure (Order { beers: beers, meal: meal })
      parseBeers s =
        (throwError ("Invalid number: " <> s))
        (Int.fromString s)

      parseMealType =
        case _ of
          "Vegan" -> pure Vegan
          "Vegetarian" -> pure Vegetarian
          "Omnivore" -> pure Omnivore
          "Carnivore" -> pure Carnivore
          s -> throwError ("Invalid meal type: " <> s)

Now we are ready to write our handler. We use parseFromForm to get a value of type Either String Order, where the String explains parsing errors. By pattern matching using record field puns, we extract the beers and meal values, and respond based on those values.

onPost =
  parseFromForm :>>=
  case _ of
    Left err ->
      writeStatus statusBadRequest
      :*> closeHeaders
      :*> respond (err <> "\n")
    Right (Order { beers, meal })
      | meal == Omnivore || meal == Carnivore ->
        writeStatus statusBadRequest
        :*> closeHeaders
        :*> respond "Sorry, we do not serve meat here.\n"
      | otherwise ->
        writeStatus statusBadRequest
        :*> closeHeaders
        :*> respond ("One " <> show meal <> " meal and "
                     <> show beers <> " beers coming up!\n")

Let’s try this server out at the command line.

$ curl -X POST -d 'beers=6' http://localhost:3000
Missing field: meal
$ curl -X POST -d 'meal=Vegan&beers=foo' http://localhost:3000
Invalid number: foo
$ curl -X POST -d 'meal=Omnivore&beers=6' http://localhost:3000
Sorry, we do not serve meat here.
$ curl -X POST -d 'meal=Vegetarian&beers=6' http://localhost:3000
One Vegetarian meal and 6 beers coming up!