Fable.Remoting is a library that enables type-safe client-server communication (RPC) for Fable and .NET Apps. It abstracts away http and lets you think of your client-server interactions only in terms of pure functions and being only a part of the webserver.
The library runs everywhere on the backend: As Suave WebPart
, as Giraffe/Saturn HttpHandler
or any other framework as Asp.net core middleware. On the client you can use Fable or .NET.
Use the SAFE Template where Fable.Remoting is a scaffolding option:
# install the template
dotnet new -i SAFE.Template
# scaffold a new Fable/Saturn project with Fable.Remoting
dotnet new SAFE --remoting
# Or use Giraffe as your server
dotnet new SAFE --server giraffe --remoting
# Or use Suave as your server
dotnet new SAFE --server suave --remoting
Library | Version |
---|---|
Fable.Remoting.Client | |
Fable.Remoting.Suave | |
Fable.Remoting.Giraffe | |
Fable.Remoting.AspNetCore | |
Fable.Remoting.DotnetClient |
Create a new F# console app:
dotnet new console -lang F#
Define the types you want to share between client and server:
// SharedTypes.fs
module SharedTypes
type Student = {
Name : string
Age : int
}
// Shared specs between Server and Client
type IStudentApi = {
studentByName : string -> Async<Student option>
allStudents : 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 SharedTypes
let 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) students
return 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 Suave
open Fable.Remotion.Server
open Fable.Remoting.Suave
let webApp : WebPart =
Remoting.createApi()
|> Remoting.fromValue studentApi
|> Remoting.buildWebPart
// start the web server
startWebServer 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 studentApi
let configureApp (app : IApplicationBuilder) =
// Add Remoting handler to the ASP.NET Core pipeline
app.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 Giraffe
open Fable.Remoting.Server
open Fable.Remoting.Giraffe
let webApp : HttpHandler =
Remoting.createApi()
|> Remoting.fromValue studentApi
|> Remoting.buildHttpHandler
let configureApp (app : IApplicationBuilder) =
// Add Giraffe to the ASP.NET Core pipeline
app.UseGiraffe webApp
let configureServices (services : IServiceCollection) =
// Add Giraffe dependencies
services.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 Saturn
open Fable.Remoting.Server
open Fable.Remoting.Giraffe
let webApp : HttpHandler =
Remoting.createApi()
|> Remoting.fromValue studentApi
|> Remoting.buildHttpHandler
let app = application {
url "http://127.0.0.1:8083/"
router webApp
}
run app
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.Client
open SharedTypes
// studentApi : IStudentApi
let studentApi =
Remoting.createApi()
|> Remoting.buildProxy<IStudentApi>
async {
// students : Student[]
let! students = studentApi.allStudents()
for student in students do
// student : Student
printfn "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:8083",// assuming the suave server is hosted op port 8083
changeOrigin: true
}
}
That's it!
- Add another record field function to
IStudentApi
- Implement that function
- Restart server
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