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#.
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.")]
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
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;")
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
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
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
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)