Skip to content

Commit

Permalink
Merge pull request #304 from martinbryant/client-server-v5
Browse files Browse the repository at this point in the history
Client server v5
  • Loading branch information
martinbryant authored Dec 8, 2023
2 parents 0287b33 + 65cccc5 commit 28b6a93
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 1 deletion.
77 changes: 77 additions & 0 deletions docs/recipes/client-server/fable-remoting.md
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.
72 changes: 72 additions & 0 deletions docs/recipes/client-server/messaging-post.md
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
}
```
124 changes: 124 additions & 0 deletions docs/recipes/client-server/messaging.md
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
```
68 changes: 68 additions & 0 deletions docs/recipes/client-server/mvu-roundtrip.md
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.
35 changes: 35 additions & 0 deletions docs/recipes/client-server/share-code.md
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.
6 changes: 5 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ nav:
- Handle server errors on the client: 'recipes/client-server/server-errors-on-client.md'
- Upload file from the client: "recipes/client-server/upload-file-from-client.md"
- Serve a file from the back-end: 'recipes/client-server/serve-a-file-from-the-backend.md'

- Add support for Fable Remoting: 'recipes/client-server/fable-remoting.md'
- Perform roundtrips with MVU: 'recipes/client-server/mvu-roundtrip.md'
- Get data from the server: 'recipes/client-server/messaging.md'
- Post data to the server: 'recipes/client-server/messaging-post.md'
- Share code between the client and the server: 'recipes/client-server/share-code.md'
- FAQs:
- Moving from dev to prod : 'faq-build.md'
- Troubleshooting : 'faq-troubleshooting.md'
Expand Down

0 comments on commit 28b6a93

Please sign in to comment.