Fable.Remoting is a RPC communication layer for Fable and .NET apps, it abstracts away Http and Json and lets you think of your client-server interactions only in terms of pure stateless functions that are statically checked at compile-time:
This interface is a record type where each field is either Async<'T>
or a function that returns Async<'T>
type IGreetingApi = { greet : string -> Async<string>}
let greetingApi = { greet = fun name ->async { let greeting = sprintf "Hello, %s" name return greeting}}// Expose the implementation as a HTTP servicelet webApp = Remoting.createApi() |> Remoting.fromValue greetingApi
// get a typed-proxy for the servicelet greetingApi = Remoting.createApi() |> Remoting.buildProxy<IGreetingApi>// Start using the serviceasync { let! message = greetingApi.greet "World" printfn "%s" message // Hello, World}
That's it, no HTTP, no JSON and it is all type-safe.
SAFE-TodoList A simple full-stack Todo list application (beginner)
tabula-rasa a real-world-ish blogging platform (intermediate)
Yobo Yoga Class Booking System implemented with Event Sourcing (advanced)
The library runs everywhere on the backend: As Suave WebPart
, as Giraffe/Saturn HttpHandler
or any other framework as Asp.NET Core middleware. Clients can be Fable or .NET application.
"Fable.Remoting solves the age-old problem of keeping your front-end code in sync with your backend code at compile time, and in a language as enjoyable to use as F#" - David Falkner
Use the SAFE Simplified template where Fable.Remoting is already set up and ready to go
Library | Version |
---|---|
Fable.Remoting.MsgPack | |
Fable.Remoting.Client | |
Fable.Remoting.Json | |
Fable.Remoting.Server | |
Fable.Remoting.Suave | |
Fable.Remoting.Giraffe | |
Fable.Remoting.AspNetCore | |
Fable.Remoting.DotnetClient | |
Fable.Remoting.AzureFunctions.Worker | |
Fable.Remoting.AwsLambda |
Create a new F# console app:
dotnet new console -lang F#
Define the types you want to share between client and server:
// SharedTypes.fsmodule SharedTypestype Student = {Name : stringAge : int}// Shared specs between Server and Clienttype IStudentApi = {studentByName : string -> Async<Student option>allStudents : unit -> Async<list<Student>>}
The type IStudentApi
is very important, this is the specification of the protocol between your server and client. Fable.Remoting
expects such type to only have functions returning Async
on the final result:
Async<A>A -> Async<B>A -> B -> Async<C>// etc...
Try to put such types in seperate files to reference these files later from the Client
Then provide an implementation for IStudentApi
on the server:
open SharedTypeslet getStudents() = async {return [{ Name = "Mike"; Age = 23; }{ Name = "John"; Age = 22; }{ Name = "Diana"; Age = 22; }] }let findStudentByName name = async {let! students = getStudents()let student = List.tryFind (fun student -> student.Name = name) studentsreturn student }let studentApi : IStudentApi = {studentByName = findStudentByName allStudents = getStudents}
Now that we have the implementation studentApi
, you can expose it as a web service from different web frameworks. We start with Suave
Install the library from Nuget using Paket:
paket add Fable.Remoting.Suave --project /path/to/Project.fsproj
Create a WebPart from the value studentApi
open Suaveopen Fable.Remoting.Serveropen Fable.Remoting.Suavelet webApp : WebPart =Remoting.createApi()|> Remoting.fromValue studentApi|> Remoting.buildWebPart// start the web serverstartWebServer defaultConfig webApp
Yes, it is that simple.
You can think of the webApp
value as if it was the following in pseudo-code:
let webApp = choose [ POST >=> path "/IStudentApi/studentByName" >=> (* deserialize request body (from json) *) >=> (* invoke studentApi.getStudentByName with the deserialized input *) >=> (* give client the output back serialized (to json) *) // other routes ]
You can enable diagnostic logging from Fable.Remoting.Server (recommended) to see how the library is doing it's magic behind the scenes :)
let webApp =Remoting.createApi()|> Remoting.fromValue studentApi|> Remoting.withDiagnosticsLogger (printfn "%s")|> Remoting.buildWebPart
Install the package from Nuget using paket
paket add Fable.Remoting.AspNetCore --project /path/to/Project.fsproj
Now you can configure your remote handler as AspNetCore middleware
let webApp =Remoting.createApi()|> Remoting.fromValue studentApilet configureApp (app : IApplicationBuilder) =// Add Remoting handler to the ASP.NET Core pipelineapp.UseRemoting webApp[<EntryPoint>]let main _ =WebHostBuilder().UseKestrel().Configure(Action<IApplicationBuilder> configureApp).Build().Run()0
You can follow the Suave part up to the library installation, where it will become:
paket add Fable.Remoting.Giraffe --project /path/to/Project.fsproj
Now instead of a WebPart, by opening the Fable.Remoting.Giraffe
namespace, you will get a HttpHandler from the value server
:
open Giraffeopen Fable.Remoting.Serveropen Fable.Remoting.Giraffelet webApp : HttpHandler =Remoting.createApi()|> Remoting.fromValue studentApi|> Remoting.buildHttpHandlerlet configureApp (app : IApplicationBuilder) =// Add Giraffe to the ASP.NET Core pipelineapp.UseGiraffe webApplet configureServices (services : IServiceCollection) =// Add Giraffe dependenciesservices.AddGiraffe() |> ignore[<EntryPoint>]let main _ =WebHostBuilder().UseKestrel().Configure(Action<IApplicationBuilder> configureApp).ConfigureServices(configureServices).Build().Run()0
You can use the same webApp
generated by the Giraffe library.
open Saturnopen Fable.Remoting.Serveropen Fable.Remoting.Giraffelet webApp : HttpHandler =Remoting.createApi()|> Remoting.fromValue studentApi|> Remoting.buildHttpHandlerlet app = application {url "http://127.0.0.1:8083/"use_router webApp}run app
To use Azure Functions in isolated mode with custom HttpTrigger as serverless remoting server, just install:
dotnet add package Fable.Remoting.AzureFunctions.Worker
or using paket
paket add Fable.Remoting.AzureFunctions.Worker --project /path/to/Project.fsproj
Since Azure Functions don't know anything about HttpHandler we need to use built-in HttpRequestData
and HttpResponseData
objects. Luckily we have Remoting.buildRequestHandler
and HttpResponseData.fromRequestHandler
functions to the rescue:
open Fable.Remoting.Serveropen Fable.Remoting.AzureFunctions.Workeropen Microsoft.Azure.Functions.Workeropen Microsoft.Azure.Functions.Worker.Httpopen Microsoft.Extensions.Loggingtype Functions(log:ILogger<Functions>) =[<Function("Index")>]member _.Index ([<HttpTrigger(AuthorizationLevel.Anonymous, Route = "{*any}")>] req: HttpRequestData, ctx: FunctionContext) =Remoting.createApi()|> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix|> Remoting.fromValue myImplementation|> Remoting.buildRequestHandler|> HttpResponseData.fromRequestHandler req
Of course, having one implementation per Function App is not ideal, so HttpResponseData.fromRequestHandlers
is here to the rescue:
type Functions(log:ILogger<Functions>) =[<Function("Index")>]member _.Index ([<HttpTrigger(AuthorizationLevel.Anonymous, Route = "{*any}")>] req: HttpRequestData, ctx: FunctionContext) =let handlerOne =Remoting.createApi()|> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix|> Remoting.fromValue myImplementationOne|> Remoting.buildRequestHandler let handlerTwo =Remoting.createApi()|> Remoting.withRouteBuilder FunctionsRouteBuilder.apiPrefix|> Remoting.fromValue myImplementationTwo|> Remoting.buildRequestHandler [ handlerOne; handlerTwo ] |> HttpResponseData.fromRequestHandlers req
Install Fable.Remoting.Client
from nuget using Paket:
paket add Fable.Remoting.Client --project /path/to/Project.fsproj
Reference the shared types to your client project
<Compile Include="path/to/SharedTypes.fs" />
Start using the library:
open Fable.Remoting.Clientopen SharedTypes// studentApi : IStudentApilet studentApi =Remoting.createApi()|> Remoting.buildProxy<IStudentApi>async { // students : Student[] let! students = studentApi.allStudents() for student in students do// student : Studentprintfn "Student %s is %d years old" student.Name student.Age}|> Async.StartImmediate
Finally, when you are using webpack-dev-server
, you have to change the config from this:
devServer: { contentBase: resolve('./public'), port: 8080}
to this:
devServer: { contentBase: resolve('./public'), port: 8080, proxy: {'/*': { // tell webpack-dev-server to re-route all requests from client to the server target: "http://localhost:5000",// assuming the backend server is hosted on port 5000 during development changeOrigin: true}}
That's it!
You can also use client functionality in non-fable projects, such as a console, desktop or mobile application.
Install Fable.Remoting.DotnetClient
from nuget using Paket:
paket add Fable.Remoting.DotnetClient --project /path/to/Project.fsproj
Reference the shared types in your client project
<Compile Include="path/to/SharedTypes.fs" />
Start using the library:
open Fable.Remoting.DotnetClientopen SharedTypes// studentApi : IStudentApilet studentApi = Remoting.createApi "http://localhost:8085" |> Remoting.buildProxy<IStudentApi>async { // students : Student[] let! students = studentApi.allStudents() for student in students do// student : Studentprintfn "Student %s is %d years old" student.Name student.Age}|> Async.StartImmediate
Notice here, that unlike the Fable client, you will need to provide the base Url of the backend because the dotnet client will be deployed separately from the backend.
Add another record field function to IStudentApi
Implement that function
Restart server and client
Done! You can now use that function from the client too.
See the following article if you are interested in how this library is implemented (a bit outdated but gives you an overview of the mechanism) Statically Typed Client-Server Communication with F#: Proof of Concept