Blog

Accelerate Your Development with FsHttp and FSharp.Data

Ditch cURL and Postman for readable HTTP queries and a parser that understands your data before you write a line of code

Open in VS Code

Open in GitHub Codespaces


So you have to hit a new JSON API. Is there an SDK library? Nope.

Forgot what flag you need for cURL? Time to use Postman. Postman asking you to sign in? Hmm... maybe you want a notebook experience? pip install requests but your Python installation is broken again....

You think to yourself,

there has to be a better way!

Well, fortunately, there is. And today I'm going to help you set it up in a matter of minutes.

Installation πŸ§‘β€πŸ’»

.NET runs on Mac?!

First, you need to install dot.net.

Now, I maybe can guess what you might be thinking.

Another SDK? Just for glorified cURL?

Maybe the best part of .NET is the developer experience. It's on your favorite package manager, there aren't any ".NET version managers" - you just install the version you want and it works, and you install tools globally, not packages, so you couldn't even break your installation just by installing packages if you tried.

Polyglot Notebooks

The .NET SDK already comes with all the F# tools we need to run these commands in the terminal with fsi, but this experience works much better with Polyglot Notebooks over VSCode with code --install-extension ms-dotnettools.dotnet-interactive-vscode.

Syntax Sugar 🍬

The first package I'd like to show is:

#r "nuget: FsHttp"
open FsHttp
Installed Packages
  • FsHttp, 14.5.1

Postman is useful because we - as humans - don't always remember the exact names of all the headers of the HTTP specification. But we kinda do at least know what a HTTP request is supposed to look like.

FsHttp uses an F# feature called computation expressions (see builder pattern, monad) to wrap System.Net.Http so it actually looks like an HTTP request.

http {
    GET "https://en.wiktionary.org/w/api.php?action=query&titles=%E5%AE%9D&prop=revisions&rvprop=content&rvsection=9&format=json"
}
|> Request.send
|> Response.toFormattedText
{
  "batchcomplete": "",
  "warnings": {
    "main": {
      "*": "Subscribe to the mediawiki-api-announce mailing list at \u003Chttps://lists.wikimedia.org/postorius/lists/mediawiki-api-announce.lists.wikimedia.org/\u003E for notice of API deprecations and breaking changes. Use [[Special:ApiFeatureUsage]] to see usage of deprecated features by your application."
    },
    "revisions": {
      "*": "Because \u0022rvslots\u0022 was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used."
    }
  },
  "query": {
    "pages": {
      "14156": {
        "pageid": 14156,
        "ns": 0,
        "title": "\u5B9D",
        "revisions": [
          {
            "contentformat": "text/x-wiki",
            "contentmodel": "wikitext",
            "*": "==Japanese==\n{{ja-kanji forms|\u5B9D|\u5BF6}}\n\n===Kanji===\n{{ja-kanji|grade=6|rs=\u5B8005|kyu=\u5BF6}}\n\n# [[precious]] [[objects]]\n# [[worldly]] [[goods]]\n# [[valuable]] [[possessions]]\n\n====Readings====\n{{ja-readings\n|goon=\u307B\u3046\n|kanon=\u307B\u3046\n|kanyoon=\u307B\n|kun=\u305F\u304B\u3089-\n}}\n\n===Etymology===\n{{ja-kanjitab|\u305F\u304B\u3089|yomi=k|alt=\u8CA1,\u8CA8}}\n\nFrom {{inherited|ja|ojp|-|sort=\u305F\u304B\u3089}}. First cited to the \u0027\u0027{{w|Man\u0027y\u014Dsh\u016B}}\u0027\u0027 of 759 {{CE}}.\u003Cref\u003E{{R:Nihon Kokugo Daijiten 2|\u5B9D\u30FB\u8CA1\u30FB\u8CA8}}\u003C/ref\u003E From {{inh|ja|jpx-pro|*takara}}.\n\nSamuel Martin analyzes this as a compound of {{m|ja|\u9AD8|tr=taka-|t=high}} \u002B {{m|ja|\u7B49|tr=-ra|pos=pluralizing suffix}}.\u003Cref\u003E{{R:ja:Martin 1987}}\u003C/ref\u003E  However, this is semantically problematic, as such a compound would ordinarily refer to \u0022the [[heights]]\u0022 as a location, and there is no clear means of deriving the sense of \u0022[[treasure]]\u0022 from the proposed component parts.\n\nSome sources derive this as a compound of {{com|ja|\u7530|tr1=ta|t1=paddy field|\u304B\u3089|tr2=kara|t2=from|lit=from the paddy fields}}, from the way people value thriving paddy fields as a unique kind of treasure.\u003Cref\u003E[https://www.takarashuzo.co.jp/takarahatakara/ \u5B9D\u306F\u7530\u304B\u3089\uFF5E\u79C1\u305F\u3061\u306E\u539F\u70B9\uFF5E | \u5B9D\u9152\u9020\u682A\u5F0F\u4F1A\u793E] - Treasure is from the rice fields -Our Beginning- | Takara Shuzo (In Japanese)\u003C/ref\u003E  However, the sense of \u0022from\u0022 for {{ja-r|\u304B\u3089}} does not appear until roughly the {{w|Heian period}},\u003Cref\u003E{{R:Nihon Kokugo Daijiten 2|\u304B\u3089}}\u003C/ref\u003E more recent than the first appearance of \u0027\u0027takara\u0027\u0027, making this a [[folk etymology]].\n\n===Pronunciation===\n{{ja-pron|\u305F\u304B\u3089|acc=3|acc_ref=DJR,SMK5,NHK}}\n\n===Noun===\n{{ja-noun|\u305F\u304B\u3089}}\n\n# {{defdate|from 759}} [[treasure]]\n\n===References===\n\u003Creferences/\u003E"
          }
        ]
      }
    }
  }
}

Postman's UI makes it ease to identify the headers you need, but Postman is less trivial to edit and document with (see Postman collections).

FsHttp requests stored in fsi/ipynb files are just that - files, so they're dead simple to edit and document, but IntelliSense makes it trivial to write the exact request you want. Fun!

bang

HTTP files can be a great alternative to Postman. Simply put - HTTP files store HTTP requests in text form, and your editor tooling can help you fill out the appropriate request and execute it.

http file UI in Visual Studio

You can even use HTTP files in the form of HttpRepl blocks in a polyglot notebook!

Start-ThreadJob -ScriptBlock {
    # Define the URL and path
    $url = "http://localhost:8080/foo"
    # Define the content to write
    $content = "So a foo walks into a bar..."
    # Create an HttpListener
    $listener = [System.Net.HttpListener]::new()
    $listener.Prefixes.Add("http://localhost:8080/")
    # Start the listener
    $listener.Start()
    Write-Host "Listening on http://localhost:8080/"
    $context = $listener.GetContext()
    $response = $context.Response
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($content)
    $response.ContentLength64 = $buffer.Length
    $response.OutputStream.Write($buffer, 0, $buffer.Length)
    $response.OutputStream.Close()
}
| Out-Null
GET http://localhost:8080

Request


GET http://localhost:8080/ HTTP/1.1

Headers
NameValue
traceparent00-d014d1bb9493ce82c59e4937d559bf7d-f62abc9ef7a72494-00
Body (0 bytes)

Response


HTTP/1.1 200 OK (590.35 ms)

Headers
NameValue
ServerMicrosoft-HTTPAPI/2.0
DateWed, 04 Sep 2024 02:37:34 GMT
Content-Length28
Body (28 bytes)So a foo walks into a bar...
Stop-Job -Id 1
Remove-Job -Id 1

So far, we haven't found a strong reason to use FsHttp over HTTP files, but there's one big advantage FsHttp has over HTTP files: programmability.

Data as First-Class Citizens πŸ—³οΈ

If you're familiar with Python or JavaScript, you might be used to dot-navigating your way through API responses but having to do data validation manually. If you've used C#, you're probably used to getting a lot of data validation for cheap with JsonDeserialize, while still having to define your schema up front with records and classes.

With F# type providers, you can actually get the benefit of both of these with neither of the costs. Let me show you what I mean:

#r "nuget: FSharp.Data"
open FSharp.Data
Installed Packages
  • FSharp.Data, 6.4.0
type GitHubRepositories = JsonProvider<
"""
{
  "items": [
    {
      "id": 29048891,
      "name": "fsharp",
      "full_name": "dotnet/fsharp"
    }
  ]
}
""">
GitHubRepositories.Load("https://api.github.com/search/repositories?q=language:fsharp&per_page=5")
  .Items
  |> Array.map (_.FullName)
[ dotnet/fsharp, fable-compiler/Fable, fsharp/fsharp, giraffe-fsharp/Giraffe, fsprojects/Paket ]

Above, we gave JsonProvider an inline sample of our schema. This sample got ingested by the build process and was used to generate the rest of the GitHubRepositories type.

Passing in hard-coded or file referenced samples like this is recommended in real applications since the data becomes part of the build process. However, for just experimenting in an interactive session, it's perfectly fine to directly pass an API endpoint to JsonProvider.

type GitHubRepositories = JsonProvider<"https://api.github.com/search/repositories?q=language:fsharp&per_page=5">
GitHubRepositories.GetSample()
    .Items
    |> Array.map (fun x -> x.FullName, x.StargazersCount)
indexvalue
0
(dotnet/fsharp, 3862)
Item1
dotnet/fsharp
Item2
3862
1
(fable-compiler/Fable, 2887)
Item1
fable-compiler/Fable
Item2
2887
2
(fsharp/fsharp, 2172)
Item1
fsharp/fsharp
Item2
2172
3
(giraffe-fsharp/Giraffe, 2106)
Item1
giraffe-fsharp/Giraffe
Item2
2106
4
(fsprojects/Paket, 2014)
Item1
fsprojects/Paket
Item2
2014

Because FsHttp is a wrapper for building requests and FSharp.Data is a library for intepreting (well, data generally, but in our case) responses, and because .NET has an awesome "work together" culture instead of "rebuild a worse version yourself 'from ground up'" culture, these two libraries work together rather flawlessly ✨:

type Kanji = JsonProvider<"https://kanjiapi.dev/v1/kanji/εŠ›">
let stream =
    http {
        GET "https://kanjiapi.dev/v1/kanji/η”Ÿ"
    }
    |> Request.send
    |> Response.toStream
let η”Ÿ = Kanji.Load(stream)
Array.concat (seq { η”Ÿ.KunReadings; η”Ÿ.OnReadings; })
[ -う, い.かす, い.きる, い.ける, う.γΎγ‚Œ, う.γΎγ‚Œγ‚‹, う.γ‚€, うま.γ‚Œγ‚‹, γ†γΎγ‚Œ, お.う, き, γͺ.す, γͺ.γ‚‹, γͺま, γͺま-, は.γˆγ‚‹, は.やす, γ‚€.す, ショウ, γ‚»γ‚€ ]

Next Steps πŸ€”

Don't wait! If a lot of things in this blog post seemed unfamiliar, you will assume they're hard and have already created a mental blockade. You will need an HTTP client later, but the energy to set up FsHttp will seem greater than dealing with the overhead of your Least Common Denominator tool, but over time that overhead will pile up and contribute to developer burn out.

Refresh your toolkit today! Start installing dot.net, take a break from the computer screen and stretch those legs, then Open in GitHub Codespaces in the browser or Open in VS Code!

Happy coding!

An unhandled error has occurred. Reload πŸ—™