We're planting a tree for every job application! Click here to learn more

POSTing to Freya

Michael Newton

15 Apr 2019

•

4 min read

POSTing to Freya
  • F#

Freya (https://freya.io/) is a modern, powerful library for web programming in F#. It will sit on top of a .NET webserver (Kestral, Suave and Katana are all specifically supported) via the OWIN standard.

Based on some of the ideas from Erlang's Webmachine, Freya allows you to define your API in terms of what should happen according to the HTTP specification. Unfortunately, while it's very powerful, it's also very new - and the current constant changes in .NET Core mean that even the documentation and examples which exist are hard to make use of.

Adding standard routing and responding to GET requests is built into the standard project template, but hooking up a JSON endpoint (a very common use case for a nice embeddable web api like this) is not.

So here's a minimal implementation of a "microservice" Freya API, starting from which dotnet commands to run to install the Freya template, through to a running web service.

Make sure you have an up to date .NET Core SDK installed, and grab yourself the handy dandy Freya template:

dotnet new -i "Freya.Template::*"

Then create yourself a directory and go into it. The following command will set up a brand new Freya project using kestrel as the underlying webserver, and Hopac (rather than F# Async) for concurrency. Alternatively, you can leave both the options off and you'll get Freya running on Suave with standard Async.

dotnet new freya --framework kestrel --concurrency hopac

Your project should run at this point; dotnet run will spin up a webserver on port 5000 which will give a 404 at the root and text responses on /hello and /hello/name paths.

Api.fs is where all the magic of configuring Freya happens - KestrelInterop.fs contains boilerplate for making sure Routing information passes correctly between Kestrel and Freya, and Program.fs just starts Kestrel with our Freya API as an OWIN app.

Adding JSON

So, this is great and all, but we're building a microservice aren't we? That normally means JSON (or at least something more structured than plain text!).

Let's change things up so that as well as supplying the name to greet in the route, we can POST JSON with a name field to the /hello end point.

To respond in JSON, we need a Freya Represent record. We're sending a result with a fixed structure, so we don't need a serialization library or anything, we'll just construct the JSON by hand. Stick this near the top of Api.fs:

open System.Text
open System.Text.RegularExpressions

let representGreeting =
    let before =
        Encoding.UTF8.GetBytes "{ \"greeting\": \""
    let after = Encoding.UTF8.GetBytes "\" }"
    let extremeSanifier =
        RegularExpressions.Regex("[^a-z0-9 ]", RegexOptions.Compiled ||| RegexOptions.IgnoreCase)
    fun name ->
        let safeNameBytes =
            extremeSanifier.Replace(name, "")
            |> Encoding.UTF8.GetBytes
        { Description =
            { Charset = Some Charset.Utf8
              Encodings = None
              MediaType = Some MediaType.Json
              Languages = None }
          Data = Array.concat [before;safeNameBytes;after] }

So here we're defining an HTTP representation of a response, including media type and other important information.

Aside: why do we return a lambda at the end rather than making representGreeting itself a function? That's so that we don't want to rebuild the two byte arrays and the regex every time we call the function.

We also need to be able to read incoming JSON. Well, all we want is a string so lets just check that there's an '"' at the beginning and end...

open System.IO

let grabString (bodyStream :# Stream) =
    use reader = new StreamReader(bodyStream)
    match reader.ReadToEnd() with
    | str when str.[0] = '"' && str.[str.Length - 1] = '"' ->
        Some <| str.Substring(1, str.Length - 2)
    | _ -> None

Now we can start hooking up the actual root that we want. We need to make some additions to helloMachine:

let helloMachine =
    freyaMachine {
        // methods [GET; HEAD; OPTIONS]
        methods [GET; HEAD; OPTIONS; POST]
        acceptableMediaTypes [MediaType.Json]
        handleOk sayHello }

Magically our endpoint now knows not only that we accept POSTs, but it will end the correct error code if the media type of the POST is not set to JSON.

We also need to update sayHello and name; we'll extract the method of the request and choose logic for working out the name and creating the response respectively.

let name_ = Route.atom_ "name"
let method_ = Freya.Optics.Http.Request.method_

let name =
    freya {
        let! requestMethod = Freya.Optic.get method_
        let! nameO =
            match requestMethod with
            | POST ->
                Freya.Optic.get Freya.Optics.Http.Request.body_
                |> Freya.map grabString
            | _ -> Freya.Optic.get name_

        match nameO with
        | Some name -> return name
        | None -> return "World" }

let representResponse greeting =
    freya {
        let! requestMethod = Freya.Optic.get method_
        match requestMethod with
        | POST ->
            return representGreeting greeting
        | _ ->
            return Represent.text greeting
    }

And that's everything we should need. Firing up PostMan we can find out that posting an empty body gets a 500 (we should probably handle that, looks like the request stream can be null), firing in a string with no media type header gets back a "415 Unsupported Media Type" (did you know that off hand?) and a POST with a correct body (i.e., starts and ends with a '"') gets us back:

{ "greeting": "Hello michael" }

So there you have it. Adding a POST endpoint to Freya.

Appendix

Here is the complete Api.fs for you to follow along, with open statements moved to the top of the file:

module Api

open System.IO
open System.Text
open System.Text.RegularExpressions
open Freya.Core
open Freya.Machines.Http
open Freya.Types.Http
open Freya.Routers.Uri.Template

let representGreeting =
    let before =
        Encoding.UTF8.GetBytes "{ \"greeting\": \""
    let after = Encoding.UTF8.GetBytes "\" }"
    let extremeSanifier =
        RegularExpressions.Regex("[^a-z0-9 ]", RegexOptions.Compiled ||| RegexOptions.IgnoreCase)
    fun name ->
        let safeNameBytes =
            extremeSanifier.Replace(name, "")
            |> Encoding.UTF8.GetBytes
        { Description =
            { Charset = Some Charset.Utf8
              Encodings = None
              MediaType = Some MediaType.Json
              Languages = None }
          Data = Array.concat [before;safeNameBytes;after] }

let grabString (bodyStream :# Stream) =
    use reader = new StreamReader(bodyStream)
    match reader.ReadToEnd() with
    | str when str.[0] = '"' && str.[str.Length - 1] = '"' ->
        Some <| str.Substring(1, str.Length - 2)
    | _ -> None


let name_ = Route.atom_ "name"
let method_ = Freya.Optics.Http.Request.method_

let name =
    freya {
        let! requestMethod = Freya.Optic.get method_
        let! nameO =
            match requestMethod with
            | POST ->
                Freya.Optic.get Freya.Optics.Http.Request.body_
                |> Freya.map grabString
            | _ -> Freya.Optic.get name_

        match nameO with
        | Some name -> return name
        | None -> return "World" }

let representResponse greeting =
    freya {
        let! requestMethod = Freya.Optic.get method_
        match requestMethod with
        | POST ->
            return representGreeting greeting
        | _ ->
            return Represent.text greeting
    }

let sayHello =
    freya {
        let! name = name

        return! representResponse (sprintf "Hello, %s!" name) }

let helloMachine =
    freyaMachine {
        // methods [GET; HEAD; OPTIONS]
        methods [GET; HEAD; OPTIONS; POST]
        acceptableMediaTypes [MediaType.Json]
        handleOk sayHello }

let root =
    freyaRouter {
        resource "/hello{/name}" helloMachine }

If you're interested in working with functional prgramming, check out our job-board here!

Did you like this article?

Michael Newton

See other articles by Michael

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub