DEV Community

Samuel Couto
Samuel Couto

Posted on

Create a F# Web API using Falco and Donald

Introduction

I have been studying F# in the last months, in this post I want to share some of my knowledge.

In this article, I’ll be covering how to create a REST API from scratch using F#, Falco and Donald.

Web API Overview

We will create an e-commerce application which has the following endpoints:

  • GET /api/products - Returns all products;
  • POST /api/products - Inserts a product;

Project Structure

I used Visual Studio Code with the extension Ionide for F#.

Image description

Project Description

  • Amazon.Catalog.Core: Project for the domain, it has domain entities classes and pure functions of the business logic.
  • Amazon.Catalog.Adapters: Project for the database's classes, in this project I am using the repository pattern and the library Donald.
  • Amazon.Catalog.WebApi: Project for controllers using the library Falco.
  • Amazon.Catalog.Application: Project for handling use cases/commands.

ShowMeTheCode

Lets start creating the Product type which is located in the assembly Amazon.Catalog.Core under the folder "Entities".

namespace Amazon.Catalog.Core.Entities

module Product =
  open System

  open Amazon.Catalog.Core

  type T = { Id: Guid 
             Name: string
             Description: string
             Price: decimal
             Active: bool }

  let validate prod =
    Utils.domainValidate prod [
      (String.noEmpty prod.Name, "Invalid name.")
      (String.noEmpty prod.Description, "Invalid description.")
      (Decimal.IsPositive prod.Price, "Invalid price.")]
Enter fullscreen mode Exit fullscreen mode

I decided to follow an approach that is more common in functional language thus I created the module Product and under it the type T which represents Product. Moreover, the module Product has the method validate that validates product's properties.

Next step is to create the ProductRepository in the assembly Amazon.Catalog.Adapters. For interacting with Postgres database I am using the library Donald.

namespace Amazon.Catalog.Adapters.Data.Repositories

module ProductRepository =
  open Donald
  open System.Data

  open Amazon.Catalog.Adapters.Data
  open Amazon.Catalog.Core
  open Amazon.Catalog.Core.Entities

  let insert(prod: Product.T) =
    Database.conn
    |> Db.newCommand "INSERT INTO public.product VALUES (@Id,@Name,@Description,@Price,@Active)"
    |> Db.setParams [
      "@Id", SqlType.Guid prod.Id
      "@Name", SqlType.String prod.Name
      "@Description", SqlType.String prod.Description
      "@Price", SqlType.Decimal prod.Price
      "@Active", SqlType.Boolean prod.Active
    ]
    |> Db.exec
    |> function
      | Ok _      -> Ok prod
      | Error err -> err |> Helper.convertDbError

  let ofDataReader (rd: IDataReader): Product.T =
    { Id          = rd.ReadGuid "Id"
      Name        = rd.ReadString "Name"
      Description = rd.ReadString "Description"
      Price       = rd.ReadDecimal "Price"
      Active      = rd.ReadBoolean "Active" }

  let get limit offset : Result<Product.T list, Error> =
    Database.conn
    |> Db.newCommand "SELECT * FROM public.product LIMIT @Limit OFFSET @Offset  "
    |> Db.setParams [
      "@Limit", SqlType.Int limit
      "@Offset", SqlType.Int offset 
    ]
    |> Db.query ofDataReader
    |> function
      | Ok prods        -> Ok prods 
      | Error err       -> err |> Helper.convertDbError
Enter fullscreen mode Exit fullscreen mode

The database connection must be provide to access it.

let conn = new NpgsqlConnection("Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=postgres;")
Enter fullscreen mode Exit fullscreen mode

Next, we will create the ´CreateProductCommand´ module, it is responsible to perform impure business logic and to call every method needed for creating a product.

namespace Amazon.Catalog.Application.Comands

[<RequireQualifiedAccess>]
module CreateProductCommand =
  open System

  open Amazon.Catalog.Core
  open Amazon.Catalog.Core.Entities
  open Amazon.Catalog.Adapters.Data.Repositories

  type Request = { Name: string
                   Description: string
                   Price: decimal }

  let createEntity req : Product.T =
    { Id          = Guid.NewGuid()
      Name        = req.Name
      Description = req.Description
      Price       = req.Price
      Active      = true }

  let checkIfProductExist (prod: Product.T) =
    ProductRepository.getByName prod.Name
    |> function
      | Ok result ->
        match result with
        | Some _ -> Error (DomainError ("Product already exists."))
        | None   -> Ok prod
      | Error err  -> Error err

  let handle (req: Request) =
    createEntity req
    |> Product.validate
    |> Result.bind checkIfProductExist
    |> Result.bind ProductRepository.insert
Enter fullscreen mode Exit fullscreen mode

Important notes:

  • The command module can have impure business logic methods;
  • The ´handle´ method is like a pipeline, it calls every function responsible to perform the given command;
  • The code is not throwing any exception, instead functions returns the type ´Result´ thus it is possible to compose functions even when the function is impure (for example ProductRepository.insert);

Once the business logic is ready, we can create the API in the Amazon.Catalog.WebApi assembly. I split the routes and controller implementation code.

In the Program module it is defined the routes.

module Amazon.Catalog.WebApi.Program

open Falco
open Falco.Routing
open Falco.HostBuilder
open Microsoft.Extensions.Logging

open Amazon.Catalog.WebApi.Controllers

[<EntryPoint>]
let main args =

  webHost args {
    endpoints [
      post "/api/products"  ProductController.create

      get "/api/products" ProductController.getProduct
    ]
  }
  0
Enter fullscreen mode Exit fullscreen mode

Inside ProductController is defined the implementation of each endpoints.

namespace Amazon.Catalog.WebApi.Controllers

module ProductController =
  open Falco

  open Amazon.Catalog.Adapters.Data.Repositories.ProductRepository
  open Amazon.Catalog.Core
  open Amazon.Catalog.WebApi.Controllers.BaseController
  open Amazon.Catalog.Application.Comands

  let getProducts: HttpHandler = fun ctx ->
    let q = Request.getQuery ctx
    let page = q.GetInt ("page", 0)
    let pageSize = q.GetInt("pageSize", 10)
    let offset = if page = 0 then page else page * pageSize
    handleResponse <| get pageSize offset <| ctx 

  let create: HttpHandler =
    let handleCreate req : HttpHandler =
      req 
      |> CreateProductCommand.handle
      |> handleResponse

    Request.mapJson handleCreate
Enter fullscreen mode Exit fullscreen mode

Falco provides different ways for getting information from the request (body, header, query string, and so on), you can check on Falco documentation.

You can find the complete code here. I am still using this project for studying F# thus feel free to contribute.

If you have any doubt or any questions comment below.

References

https://fsharpforfunandprofit.com/posts/organizing-functions/
https://dev.to/tunaxor/f-s-mean-1g2b

Top comments (0)