Introduction to Web Programming in F# with Giraffe – Part 2
Introduction
In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.
In this post, we will creating a simple API. In later posts, we will delve a lot deeper into Giraffe but this will be a gentle but useful start.
If you haven't already done so, read the first post in this series.
Getting Started
Ideally, you should have .NET 5 SDK installed. If you haven't, it will still work for .NET 3.1 SDK.
I suggest that you use VSCode with the ionide F# extension. They work on Windows, MacOS and Linux.
Create a new folder called GiraffeApi and open it in VSCode.
Using the Terminal in VSCode type in the following command to create the project:
dotnet new console -lang F#
After a few seconds, the ionide extension will spring to life. When it does, add the following NuGet packages from the terminal:
dotnet add package Giraffe -v 5.0.0-rc-6
open Program.fs and replace the code with the code from this gist:
https://gist.github.com/ianrussellsoftwarepark/8b02b1e07c65e956d921eac882d08f2b
Running the Sample Code
In the Terminal, type the following to run the project:
dotnet run
Go to your browser and type in the following Url:
https://localhost:5001
You should see some text.
Now try the following Uri and you should see some Json returned:
https://localhost:5001/api
You will need a tool to run HTTP calls (GET, POST, PUT, and DELETE). I use Postman but any tool including those available in the IDEs will work.
Our Task
We are going to create a simple API that we can view, create, update and delete Todo items.
Data
Rather than work against a real data store, we are going to create a simple store with a dictionary and use that in our handlers.
Create a new file above Program.fs called TodoStore.fs
and add the following code to it:
module Todos open System open System.Collections.Concurrent type TodoId = Guid type NewTodo = { Description: string } type Todo = { Id: TodoId Description: string Created: DateTime IsCompleted: bool } type TodoStore() = let data = ConcurrentDictionary<TodoId, Todo>() member _.Create todo = data.TryAdd(todo.Id, todo) member _.Update todo = data.TryUpdate(todo.Id, todo, data.[todo.Id]) member _.Delete id = data.TryRemove id member _.Get id = data.[id] member _.GetAll () = data.ToArray()
This is not production code. TodoStore is a simple class type that wraps a concurrent dictionary that we can test our API out with. It will not persist between runs. To plug it in, we need to make a change to configureServices
in Program.fs:
let configureServices (services : IServiceCollection) = services.AddGiraffe() .AddSingleton<TodoStore>(TodoStore()) |> ignore
We add the TodoStore as a singleton as we only want one instance to exist. If you're thinking that this looks like dependency injection, you would be correct; It is!
You'll need to add an open Todos
to the top of the file as well.
Routes
We saw in the last post that Giraffe uses individual route handlers, so we need to think about how to add our new routes.
If we request GET /api/fred
from the following routing:
let webApp = choose [ GET >=> route "/" >=> htmlView todoView subRoute "/api" (choose [ GET >=> route "" >=> json { Response = "ToDo List API" } GET >=> routef "/%s" sayHelloNameHandler ]) setStatusCode 404 >=> text "Not Found" ]
We will hit the route that calls the sayHelloNameHandler
handler. If we call a POST, no routes will match, so we will fall through to the last line and return a 404 - Not found.
The routes we need to add are:
GET /api/todo // Get a list of todos GET /api/todo/id // Get one todo PUT /api/todo // Create a todo POST /api/todo // Update a todo DELETE /api/todo/id // Delerte a todo
Let's create our routes with the correct HTTP verbs:
let apiTodoRoutes : HttpHandler = subRoute "/todo" (choose [ GET >=> choose [ routef "/%O" Handlers.viewTaskHandler route "" >=> Handlers.viewTasksHandler ] POST >=> route "" >=> Handlers.updateTaskHandler PUT >=> route "" >=> Handlers.createTaskHandler DELETE >=> routef "/%O" Handlers.deleteTaskHandler ]) let webApp = choose [ GET >=> route "/" >=> htmlView todoView subRoute "/api" (choose [ apiTodoRoutes GET >=> route "" >=> json { Response = "ToDo List API" } GET >=> routef "/%s" Handlers.sayHelloNameHandler ]) setStatusCode 404 >=> text "Not Found" ]
Our todo routes live under the api subroute. We need to put the apiTodoRoutes route handler before the existing ones because the sayHelloNameHandler will be selected instead of our GET /api/todos route and return:
{ "Response": "Hello, todos" }
Next we have to implement the new handlers.
Handlers
Create a module above the routes called Handlers
and add the sayHelloNameHandler function to it.
module Handlers = let sayHelloNameHandler (name:string) = fun (next : HttpFunc) (ctx : HttpContext) -> task { let msg = sprintf "Hello, %s" name return! json { Response = msg } next ctx }
Let's add the GET routes to our module:
let viewTasksHandler = fun (next : HttpFunc) (ctx : HttpContext) -> task { let store = ctx.GetService<TodoStore>() let todos = store.GetAll() return! json todos next ctx } let viewTaskHandler (id:Guid) = fun (next : HttpFunc) (ctx : HttpContext) -> task { let store = ctx.GetService<TodoStore>() let todo = store.Get(id) return! json todo next ctx }
We are using the context (ctx) to gain access to the TodoStore instance we set up earlier.
Let's add the remaining handler for create, update and delete:
let createTaskHandler = fun (next : HttpFunc) (ctx : HttpContext) -> task { let! newTodo = ctx.BindJsonAsync<NewTodo>() let store = ctx.GetService<TodoStore>() let created = store.Create({ Id = Guid.NewGuid(); Description = newTodo.Description; Created = DateTime.UtcNow; IsCompleted = false }) return! json created next ctx } let updateTaskHandler = fun (next : HttpFunc) (ctx : HttpContext) -> task { let! todo = ctx.BindJsonAsync<Todo>() let store = ctx.GetService<TodoStore>() let created = store.Update(todo) return! json created next ctx } let deleteTaskHandler (id:Guid) = fun (next : HttpFunc) (ctx : HttpContext) -> task { let store = ctx.GetService<TodoStore>() let existing = store.Get(id) let deleted = store.Delete(KeyValuePair<TodoId, Todo>(id, existing)) return! json deleted next ctx }
The most interesting thing here is that we use a built-in function to gain strongly-typed access to the body passed into the handler.
Using the API
Run the app and use a tool like Postman to work with the API.
To get a list of all Todos, we call GET /api/todo
. This should return an empty json array.
We create a Todo by calling PUT /api/todo
with a json body like this:
{ "Description": "Finish blog post" }
You will receive a reponse of true. If you now call the list again, you will receive a json response like this:
[ { "key": "ff5a1d35-4573-463d-b9fa-6402202ab411", "value": { "id": "ff5a1d35-4573-463d-b9fa-6402202ab411", "description": "Finish blog post", "created": "2021-03-12T13:47:39.3564455Z", "isCompleted": false } } ]
I'll leave the other routes for you to investigate.
The code for this post is available here:
https://gist.github.com/ianrussellsoftwarepark/f1c0815efe309ee6dd4bebf397d75f8d
Summary
I hope that you found this post in the Introduction to Web Programming in F# with Giraffe series useful and interesting. We have only scratched the surface of what is possible with Giraffe for creating APIs.
In the next post we will start to investigate HTML views with the Giraffe View Engine.
If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.