-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #304 from martinbryant/client-server-v5
Client server v5
- Loading branch information
Showing
6 changed files
with
381 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# How Do I Add Support for Fable Remoting? | ||
[Fable Remoting](https://zaid-ajaj.github.io/Fable.Remoting/) is a type-safe RPC communication layer for SAFE apps. It uses HTTP behind the scenes, but allows you to program against protocols that exist across the application without needing to think about the HTTP plumbing, and is a great fit for the majority of SAFE applications. | ||
|
||
> Note that the standard template uses Fable Remoting. **This recipe only applies to the minimal template**. | ||
#### 1. Install NuGet Packages | ||
Add [Fable.Remoting.Giraffe](https://www.nuget.org/packages/Fable.Remoting.Giraffe/) to the Server and [Fable.Remoting.Client](https://www.nuget.org/packages/Fable.Remoting.Client/) to the Client. | ||
|
||
> See [How Do I Add a NuGet Package to the Server](../../package-management/add-nuget-package-to-server) | ||
> and [How Do I Add a NuGet Package to the Client](../../package-management/add-nuget-package-to-client). | ||
#### 2. Create the API protocol | ||
You now need to create the protocol, or contract, of the API we’ll be creating. Insert the following below the `Route` module in `Shared.fs`: | ||
```fsharp | ||
type IMyApi = | ||
{ hello : unit -> Async<string> } | ||
``` | ||
|
||
#### 3. Create the routing function | ||
We need to provide a basic routing function in order to ensure client and server communicate on the | ||
same endpoint. Find the `Route` module in `src/Shared/Shared.fs` and replace it with the following: | ||
|
||
```fsharp | ||
module Route = | ||
let builder typeName methodName = | ||
sprintf "/api/%s/%s" typeName methodName | ||
``` | ||
|
||
#### 4. Create the protocol implementation | ||
We now need to provide an implementation of the protocol on the server. Open `src/Server/Server.fs` and insert the following right after the `open` statements: | ||
|
||
```fsharp | ||
let myApi = | ||
{ hello = fun () -> async { return "Hello from SAFE!" } } | ||
``` | ||
|
||
#### 5. Hook into ASP.NET | ||
We now need to "adapt" Fable Remoting into the ASP.NET pipeline by converting it into a Giraffe HTTP Handler. Don't worry - this is not hard. Find `webApp` in `Server.fs` and replace it with the following: | ||
|
||
```fsharp | ||
open Fable.Remoting.Server | ||
open Fable.Remoting.Giraffe | ||
let webApp = | ||
Remoting.createApi() | ||
|> Remoting.withRouteBuilder Route.builder // use the routing function from step 3 | ||
|> Remoting.fromValue myApi // use the myApi implementation from step 4 | ||
|> Remoting.buildHttpHandler // adapt it to Giraffe's HTTP Handler | ||
``` | ||
|
||
#### 6. Create the Client proxy | ||
We now need a corresponding client proxy in order to be able to connect to the server. Open `src/Client/Client.fs` and insert the following right after the `Msg` type: | ||
```fsharp | ||
open Fable.Remoting.Client | ||
let myApi = | ||
Remoting.createApi() | ||
|> Remoting.withRouteBuilder Route.builder | ||
|> Remoting.buildProxy<IMyApi> | ||
``` | ||
|
||
#### 7. Make calls to the Server | ||
Replace the following two lines in the `init` function in `Client.fs`: | ||
|
||
```fsharp | ||
let getHello() = Fetch.get<unit, string> Route.hello | ||
let cmd = Cmd.OfPromise.perform getHello () GotHello | ||
``` | ||
|
||
with this: | ||
|
||
```fsharp | ||
let cmd = Cmd.OfAsync.perform myApi.hello () GotHello | ||
``` | ||
|
||
## Done! | ||
At this point, the app should work just as it did before. Now, expanding the API and adding a new endpoint is as easy as adding a new field to the API protocol we defined in `Shared.fs`, editing the `myApi` record in `Server.fs` with the implementation, and finally making calls from the proxy. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# How do I send and receive data using POST? | ||
This recipe shows how to create an endpoint on the server and hook up it up to the client using HTTP POST. This recipe assumes that you have also followed [this](mvu-roundtrip.md) recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server. | ||
|
||
A POST endpoint is normally used to send data from the client to the server in the body, for example from a form. This is useful when we need to supply more data than can easily be provided in the URI. | ||
|
||
> You may wish to use POST for "write" operations and use GETs for "reads", however this is a highly opinionated topic that is beyond the scope of this recipe. | ||
## **I'm using the standard template** (Fable Remoting) | ||
Fable Remoting takes care of deciding whether to use POST or GET etc. - you don't have to worry about this. Refer to [this](messaging.md) recipe for more details. | ||
|
||
## **I'm using the minimal template** (Raw HTTP) | ||
|
||
### In Shared | ||
#### 1. Create contract | ||
Create the type that will store the payload sent from the client to the server. | ||
|
||
```fsharp | ||
type SaveCustomerRequest = | ||
{ Name : string | ||
Age : int } | ||
``` | ||
|
||
### On the Client | ||
#### 1. Call the endpoint | ||
Create a new function `saveCustomer` that will call the server. It supplies the customer to save, which | ||
is serialized and sent to the server in the body of the message. | ||
|
||
```fsharp | ||
let saveCustomer customer = | ||
let save customer = Fetch.post<SaveCustomerRequest, int> ("/api/customer", customer) | ||
Cmd.OfPromise.perform save customer CustomerSaved | ||
``` | ||
|
||
> The generic arguments of `Fetch.post` are the input and output types. The example above shows that | ||
> the input is of type `SaveCustomerRequest` with the response will contain an integer value. This may | ||
> be the ID generated by the server for the save operation. | ||
This can now be called from within your `update` function e.g. | ||
|
||
```fsharp | ||
| SaveCustomer request -> | ||
model, saveCustomer request | ||
| CustomerSaved generatedId -> | ||
{ model with GeneratedCustomerId = Some generatedId; Message = "Saved customer!" }, Cmd.none | ||
``` | ||
|
||
### On the Server | ||
#### 1. Write implementation | ||
Create a function that can extract the payload from the body of the request using Giraffe's built-in [model binding support](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#model-binding): | ||
|
||
```fsharp | ||
open FSharp.Control.Tasks | ||
open Giraffe | ||
open Microsoft.AspNetCore.Http | ||
open Shared | ||
/// Extracts the request from the body and saves to the database. | ||
let saveCustomer next (ctx:HttpContext) = task { | ||
let! customer = ctx.BindModelAsync<SaveCustomerRequest>() | ||
do! Database.saveCustomer customer | ||
return! Successful.OK "Saved customer" next ctx | ||
} | ||
``` | ||
|
||
#### 2. Expose your function | ||
Tie your function into the router, using the `post` verb instead of `get`. | ||
|
||
```fsharp | ||
let webApp = router { | ||
post "/api/customer" saveCustomer // Add this | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# How do I send and receive data? | ||
This recipe shows how to create an endpoint on the server and hook up it up to the client. This recipe assumes that you have also followed [this](mvu-roundtrip.md) recipe and have an understanding of MVU messaging. This recipe only shows how to wire up the client and server. | ||
|
||
## **I'm using the standard template** (Fable Remoting) | ||
[Fable Remoting](https://zaid-ajaj.github.io/Fable.Remoting/) is a library which allows you to create client/server messaging without any need to think about HTTP verbs or serialization etc. | ||
|
||
### In Shared | ||
#### 1. Update contract | ||
Add your new endpoint onto an existing API contract e.g. `ITodosApi`. Because Fable Remoting exposes your API through F# on client and server, you get type safety across both. | ||
|
||
```fsharp | ||
type ITodosApi = | ||
{ getCustomer : int -> Async<Customer option> } | ||
``` | ||
|
||
### On the server | ||
#### 1. Write implementation | ||
Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. | ||
```fsharp | ||
let loadCustomer customerId = async { | ||
return Some { Name = "My Customer" } | ||
} | ||
``` | ||
|
||
> Note the use of `async` here. Fable Remoting uses async workflows, and not tasks. You *can* write functions that use task, but will have to at some point map to async using `Async.AwaitTask`. | ||
#### 2. Expose your function | ||
Tie the function you've just written into the API implementation. | ||
```fsharp | ||
let todosApi = | ||
{ ///... | ||
getCustomer = loadCustomer | ||
} | ||
``` | ||
|
||
#### 3. Test the endpoint (optional) | ||
Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. See [here](https://zaid-ajaj.github.io/Fable.Remoting/src/raw-http.html) for more details on the required format. | ||
|
||
### On the client | ||
#### 1. Call the endpoint | ||
Create a new function `loadCustomer` that will call the endpoint. | ||
|
||
```fsharp | ||
let loadCustomer customerId = | ||
Cmd.OfAsync.perform todosApi.getCustomer customerId LoadedCustomer | ||
``` | ||
|
||
> Note the final value supplied, `CustomerLoaded`. This is the `Msg` case that will be sent into the | ||
> Elmish loop *once the call returns*, with the returned data. It should take in a value that | ||
> matches the type returned by the Server e.g. `CustomerLoaded of Customer option`. See [here](mvu-roundtrip.md) | ||
> for more information. | ||
This can now be called from within your `update` function e.g. | ||
|
||
```fsharp | ||
| LoadCustomer customerId -> | ||
model, loadCustomer customerId | ||
``` | ||
|
||
## **I'm using the minimal template** (Raw HTTP) | ||
This recipe shows how to create a GET endpoint on the server and consume it on the client using the Fetch API. | ||
|
||
### On the Server | ||
#### 1. Write implementation | ||
Create a function that implements the back-end service that you require. Use standard functions to read from databases or other external sources as required. | ||
```fsharp | ||
open Saturn | ||
open FSharp.Control.Tasks | ||
/// Loads a customer from the DB and returns as a Customer in json. | ||
let loadCustomer (customerId:int) next ctx = task { | ||
let customer = { Name = "My Customer" } | ||
return! json customer next ctx | ||
} | ||
``` | ||
> Note how we parameterise this function to take in the `customerId` as the first argument. Any parameters you need should be supplied in this manner. If you do not need any parameters, just omit them and leave the `next` and `ctx` ones. | ||
> This example does not cover dealing with "missing" data e.g. invalid customer ID is found. | ||
#### 2.Expose your function | ||
Tie the function into the router with a route. | ||
|
||
```fsharp | ||
let webApp = router { | ||
getf "/api/customer/%i" loadCustomer // Add this | ||
} | ||
``` | ||
|
||
> Note the use of `getf` rather than `get`. If you do not need any parameters, just use `get`. See [here](https://saturnframework.org/reference/Saturn/saturn-router-routerbuilder.html) for reference docs on the use of the Saturn router. | ||
#### 3. Test the endpoint (optional) | ||
Test out your endpoint using e.g. web browser / Postman / REST Client for VS Code etc. | ||
|
||
### On the client | ||
#### 1. Call the endpoint | ||
Create a new function `loadCustomer` that will call the endpoint. | ||
|
||
> This example uses [Thoth.Fetch](https://thoth-org.github.io/Thoth.Fetch/index.html) to download and deserialise the response. | ||
```fsharp | ||
let loadCustomer customerId = | ||
let loadCustomer () = Fetch.get<unit, Customer> (sprintf "/api/customer/%i" customerId) | ||
Cmd.OfPromise.perform loadCustomer () CustomerLoaded | ||
``` | ||
|
||
> Note the final value supplied, `CustomerLoaded`. This is the `Msg` case that will be sent into the | ||
> Elmish loop *once the call returns*, with the returned data. It should take in a value that | ||
> matches the type returned by the Server e.g. `CustomerLoaded of Customer`. See [here](mvu-roundtrip.md) | ||
> for more information. | ||
An alternative (and slightly more succinct) way of writing this is: | ||
|
||
```fsharp | ||
let loadCustomer customerId = | ||
let loadCustomer = sprintf "/api/customer/%i" >> Fetch.get<unit, Customer> | ||
Cmd.OfPromise.perform loadCustomer customerId CustomerLoaded | ||
``` | ||
|
||
This can now be called from within your `update` function e.g. | ||
|
||
```fsharp | ||
| LoadCustomer customerId -> | ||
model, loadCustomer customerId | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# How do I load data from server to client using MVU? | ||
This recipe demonstrates the steps you need to take to store new data on the client using the MVU pattern, which is typically read from the Server. You will learn the steps required to modify the model, update and view functions to handle a button click which requests data from the server and handles the response. | ||
|
||
## In Shared | ||
#### 1. Create shared domain | ||
Create a type in the Shared project which will act as the contract type between client and server. As SAFE compiles F# into JavaScript for you, you only need a single definition which will automatically be shared. | ||
```fsharp | ||
type Customer = { Name : string } | ||
``` | ||
|
||
## On the Client | ||
#### 1. Create message pairs | ||
Modify the `Msg` type to have two new messages: | ||
|
||
```fsharp | ||
type Msg = | ||
// other messages ... | ||
| LoadCustomer of customerId:int // Add this | ||
| CustomerLoaded of Customer // Add this | ||
``` | ||
|
||
> You will see that this symmetrical pattern is often followed in MVU: | ||
> | ||
> * A *command* to initiate a call to the server for some data (**Load**Customer) | ||
> * An *event* with the result of calling the command (Customer**Loaded**) | ||
#### 2. Update the Model | ||
Update the Model to store the Customer once it is loaded: | ||
```fsharp | ||
type Model = | ||
{ // ... | ||
TheCustomer : Customer option } | ||
``` | ||
|
||
> Make `TheCustomer` optional so that it can be initialised as `None` (see next step). | ||
#### 3. Update the Init function | ||
Update the `init` function to provide default data | ||
```fsharp | ||
let model = | ||
{ // ... | ||
TheCustomer = None } | ||
``` | ||
#### 4. Update the View | ||
Update your view to initiate the `LoadCustomer` event. Here, we create a button that will start loading customer 42 on click: | ||
```fsharp | ||
let view model dispatch = | ||
Html.div [ | ||
// ... | ||
Html.button [ | ||
prop.onClick (fun _ -> dispatch (LoadCustomer 42)) | ||
prop.text "Load Customer" | ||
] | ||
] | ||
``` | ||
#### 5. Handle the Update | ||
Modify the `update` function to handle the new messages: | ||
```fsharp | ||
let update msg model = | ||
match msg with | ||
// .... | ||
| LoadCustomer customerId -> | ||
// Implementation to connect to the server to be defined. | ||
| CustomerLoaded c -> | ||
{ model with TheCustomer = Some c }, Cmd.none | ||
``` | ||
|
||
> The code to fire off the message to the server differs depending on the client / server communication you are using and normally whether you are reading or writing data. See [here](messaging.md) for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# How Do I Share Code Types Between the Client and the Server? | ||
SAFE Stack makes it really simple and easy to share code between the client and the server, since both of them are written in F#. The client side is transpiled into JavaScript via webpack, whilst the server side is compiled down to .NET CIL. Serialization between both happens in the background, so you don't have to worry about it. | ||
|
||
--- | ||
|
||
## Types | ||
Let’s say the you have the following type in `src/Server/Server.fs`: | ||
```fsharp | ||
type Customer = | ||
{ Id : Guid | ||
Name : string | ||
Email : string | ||
PhoneNumber : string } | ||
``` | ||
|
||
## Values and Functions | ||
And you have the following function that is used to validate this Customer type in `src/Client/Index.fs`: | ||
```fsharp | ||
let customerIsValid customer = | ||
(Guid.Empty = customer.Id | ||
|| String.IsNullOrEmpty customer.Name | ||
|| String.IsNullOrEmpty customer.Email | ||
|| String.IsNullOrEmpty customer.PhoneNumber) | ||
|> not | ||
``` | ||
|
||
## Shared | ||
If at any point you realise you need to use both the `Customer` type and the `customerIsValid` function both in the Client and the Server, all you need to do is to move both of them to `Shared` project. You can either put them in the `Shared.fs` file, or create your own file in the Shared project (eg. `Customer.fs`). After this, you will be able to use both the `Customer` type and the `customerIsValid` function in both the Client and the Server. | ||
|
||
## Serialization | ||
SAFE comes out of the box with [Fable.Remoting] or [Thoth] for serialization. These will handle transport of data seamlessly for you. | ||
|
||
## Considerations | ||
|
||
> Be careful not to place code in `Shared.fs` that depends on a Client or Server-specific dependency. If your code depends on `Fable` for example, in most cases it will not be suitable to place it in Shared, since it can only be used in Client. Similarly, if your types rely on .NET specific types in e.g. the framework class library (FCL), beware. Fable [has built-in mappings](https://fable.io/docs/dotnet/compatibility.html) for popular .NET types e.g. `System.DateTime` and `System.Math`, but you will have to write your own mappers otherwise. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters