United Kingdom: +44 (0)208 088 8978

Adopting SAFE Meta-packages

Simplifying a SAFE Stack From Scratch project by adopting SAFE meta-packages

We're hiring Software Developers

Click here to find out more

When I'm learning something new, I find it really helps to know what is happening under the covers. That's the approach I've taken to learning to use SAFE Stack. The template is fantastically easy to use and the dojo exercises are very helpful in beginning to navigate the code.

Coming primarily from a back-end and systems integration development background, I wanted to build a deep understanding of the magic behind the scenes. The SAFE Stack docs had me covered! Creating a SAFE Stack App from Scratch took me in digestible steps from an empty repository to a Client SPA with React and Elmish, a Server API, and Fable Remoting to simplify communication. Then I used the recipes in "How do I..." to add DaisyUI and FontAwesome support, switch from Nuget to Paket, and so on.

I wanted to continue the project I've been developing as part of this experience, but also catch up with some features in the template that aren't yet covered in the recipes - particularly I wanted to adopt the SAFE.Server and SAFE.Client meta-packages for simplified package management and a few extra helper types to reduce the boilerplate in client-server communication. We've introduced these packages on the blog already.

Migration to SAFE Stack meta-packages

In the server project, I added SAFE.Server and removed the redundant references.

dotnet paket add SAFE.Server --project server

dotnet paket remove Fable.Remoting.Giraffe --project server
dotnet paket remove Fable.Remoting.Server --project server
dotnet paket remove Giraffe --project server
dotnet paket remove FSharp.Core --project server
dotnet paket remove Newtonsoft.Json --project server

I did the same for the client project.

dotnet paket add SAFE.Client --project client

dotnet paket remove FSharp.Core --project client
dotnet paket remove Feliz --project client
dotnet paket remove Fable.Remoting.Client --project client
dotnet paket remove Fable.Elmish.HMR --project client
dotnet paket remove Fable.Elmish.React --project client

Actually, I also removed Fable.Elmish.Debugger from the client but then added it back in. it's not included by the SAFE.Client meta-package as it's typically only desirable during development.

Beyond Meta-Packages: Using API Helpers

We mentioned that the SAFE.Client and SAFE.Server packages aren't strictly meta-packages; they also provide helper functions that help us simplify the code we write to wire up client-server communication using Fable Remoting.

Joost covered two really useful types in SAFE.Client that simplify calling APIs and managing state while waiting for the response in his posts dealing with remote data using SAFE.Client.

In addition to that, we can take advantage of API.make from SAFE.Server, and API.makeProxy from SAFE.Client.

Switching to Api.make in the Server project

Here's part of the startup code from my Server project prior to switching to SAFE.Server:

let webApp: HttpHandler =
    Remoting.createApi()
    |> Remoting.fromValue MyApi.myApi
    |> Remoting.buildHttpHandler

And after switching:

let webApp = Api.make MyApi.myApi

These two snippets have different defaults for the route to the API so they are not exactly equivalent. I could pass a routeBuilder function to Api.make to override its default /api/IMyApi/EndPoint route to just /IMyApi/EndPoint, but the changes on the client side will have corresponding defaults so in this case it's not necessary. If I had been using a routeBuilder in the original Fable handler setup I could pass the same function to Api.make.

Use Api.makeProxy in the Client project

As with the Server project, the Client project can be simplified from:

let myApi: IMyApi =
    Remoting.createApi()
    |> Remoting.buildProxy<IMyApi>

To:

let myApi: IMyApi = Api.makeProxy<IMyApi> ()

It's a small change but it's satisfying!

Finally, because I was happy with the route change I just had to make sure the Vite development server would proxy requests to /api to the backend when running locally. It's one line in vite.config.mts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'

const proxyPort = process.env.SERVER_PROXY_PORT || "5000";
const proxyTarget = "http://localhost:" + proxyPort;

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [react()],
    server: {
        port: 8080,
        proxy: {
-            "/IMyApi/": {
+            "/api/": {
                target: proxyTarget,
                changeOrigin: true,
            }
        }
    }
});

Conclusion

Adopting the SAFE.Client and SAFE.Server meta-packages has simplified the dependencies in my projects.

Api.make and Api.makeProxy reduced the boilerplate in setting up Fable Remoting. Of course I also made use of ApiCall and ReturnData types in my client project, as Joost has already described on this blog.

I was very pleased I could continue working on the same project I started to build my understanding of SAFE Stack, while bringing it closer to the standard SAFE template.