Yesterday night I was about to demo a quick server/client pair with Freya and Fable, and it all went a bit wrong. Some of the issues weren't related to what I did (computers, gotta love 'em) but others were just bits of configuration that I didn't have at my finger tips.
This means it's time for a little practice for me, and a mini-tutorial for you (and future me).
What we're going to do
We're going to build a small server application based on Freya which will serve JSON and be a nice RESTful (in the loose sense) API.
Then, we're going to configure Fable with Elmish to load data from that API. The crucial thing here is that we're going to configure both projects such that we have a seamless development work flow; automated recompile and restart of the server on code changes, and automatic recompile/reload of the Fable UI on change.
The server
Make sure your dotnet core Freya template is up to date:
1
dotnet new -i Freya
In a root directory for our overall solution, run:
This will create a new directory called "FateServer" with a F# project in it. Go into the directory and make sure everything has restored correctly:
123
cd FateServer
dotnet restore
dotnet build
One thing I've been slowly learning with dotnet core is that the restore run by default during a build doesn't always seem to be as effective as actually running the full restore command. Just in general, if Core is behaving strangely, running restore is a good starting point.
Next up is making our server log something: by default, Kestrel logs basically nothing.
Install the logging package (it's not part of the default Freya template):
Run dotnet restore and from now on running dotnet watch run to start continuous development with file watching should work.
Now we just need to serve up some JSON. We want a send a format which Fable understands, and the kind people at the Fable project have written a Newtonsoft configuration for doing exactly that.
Next, set up the domain. Create a new file Character.fs (we're going to be sending back and forth Fate Accelerated characters as data). Make sure you add it to the project file before Api.fs.
Now move across to Api.fs. You'll see that it defaults to a single "greeting" endpoint which responds with a text response. Let's add a helper for sending JSON correctly, immediately after the existing open statements:
// This endpoint requires a URL template with the "name" atomletname_=Route.atom_"name"letname=freya{let!nameO=Freya.Optic.getname_matchnameOwith|Somename->returnname|None->returnfailwith"Name is a compulsory element of the URL"}// We're going to hard code our data for nowletexampleCharacters=Map["bob",{Name="Bob Bobson"Careful=MediocreClever=FairFlashy=FairForceful=AverageQuick=AverageSneaky=GoodHighConcept="The eternal example"Trouble="Lives in the test"Aspects=["It's only Bob""Is he... the recursive one?""I've got Fred's back!"]Stunts=["Because everyone assumes I don't exist, I get +2 on Sneaky rolls to not be noticed."]}]// Once per request, try and load the named character (see the memo at the end)letcharacter=freya{let!name=namereturnMap.tryFind(name.ToLowerInvariant())exampleCharacters}|>Freya.memoletcharacterExists=character|>Freya.map(func->c.IsSome)letsendCharacter=freya{let!character=characterreturnRepresent.json(character.Value)}letcharacterMachine=freyaMachine{#ifDEBUGcorscorsOrigins[SerializedOrigin.parse"http://localhost:8080"]#endifmethods[GET;HEAD;OPTIONS]existscharacterExistshandleOksendCharacter}letroot=freyaRouter{resource"/character/{name}"characterMachine}
There's quite a lot going on in there, but what we've defined with characterMachine is a resource which checks if a character exists, and sends it as Fable readable JSON if it does. We then configure a route to point to it.
Critically, we also turn on CORS (Cross Origin Resource Sharing) for localhost:8080 for debug builds. This will enable requests from our Fable client running it's development server on a different port to talk to the server.
Edit: Zaid Ajaj points out that you can also configure webpack's dev server to proxy to your development front end. If you're writing a system where your API and client will be running on the same domain, check out how to do that below.
The client
Go back up into the root directory of the solution, and run:
1
dotnet new -i Fable.Template.Fulma.Minimal
To get a dotnet core template for Fable with F# wrappers for React and Bulma - as well as Elmish pre-installed.
Then run:
1
dotnet new fulma-minimal -lang f# -o FateClient
To create our client application.
Go into the newly created project directory, and use the built in build scripts to get everything up and running:
12
cd FateClient
./fake.sh -t watch
On first run, it will download most of the internet, but such is modern net development.
Browse on over to http://localhost:8080/ to see the base template before we start hacking away!
Very pretty: and in App.fs we can see the nice clean Elmish code driving it.
If you're running both API and client on the same domain, this is also a good time to update your webpack config (you'll find webpack.config.js in your FateClient directory). Amend the devServer section as follows:
If you do this, you'll want to change the URL below used to load the data.
Now! Let's start hacking away. Firstly, we're going to want to share our character types. I've decided here that they are owned by the server, so we need to link the file into the Fable project.
moduleApp.ViewopenElmishopenFable.Helpers.ReactopenFable.Helpers.React.PropsopenFable.PowerPackopenFable.PowerPack.FetchopenFulmaopenFulma.FontAwesomeopenCharactertypeModel={IsLoading:boolCharacter:CharacteroptionErrorMessage:stringoption}typeMsg=|CharacterLoadedofCharacter|LoadingErrorofstringletloadBob()=promise{letprops=[RequestProperties.MethodHttpMethod.GET]#ifDEBUG// Use "/character/bob" here if you've set up the webpack proxyreturn!fetchAs<Character>"http://localhost:5000/character/bob"props#elsereturn!fetchAs<Character>"http://api.example.com/character/bob"props#endif}letinit_={IsLoading=trueCharacter=NoneErrorMessage=None},Cmd.ofPromiseloadBob()CharacterLoaded(fune->LoadingErrore.Message)letprivateupdatemsgmodel=matchmsgwith|CharacterLoadedbob->{modelwithIsLoading=falseCharacter=Somebob},Cmd.none|LoadingErrorerror->{modelwithIsLoading=falseCharacter=NoneErrorMessage=Someerror},Cmd.noneletloadingMessagemodel=ifmodel.IsLoadingthen[str"Loading..."]else[]letisRounded:IHTMLProplist=[Style[BorderRadius"25px"]]letcharacterViewcharacter=[Hero.hero[Hero.ColorIsBlackHero.PropsisRounded][Hero.body[][Container.container[Container.IsFluidContainer.Modifiers[Modifier.TextAlignment(Screen.All,TextAlignment.Centered)]][Heading.h1[][strcharacter.Name]p[][strong[][str"High Concept: "]strcharacter.HighConcept]p[][strong[][str"Trouble: "]strcharacter.Trouble]]]]Columns.columns[][Column.column[][Heading.h2[][str"Approaches"]Table.table[Table.IsBorderedTable.IsStriped][thead[][tr[][th[][str"Approach"]th[][str"Level"]]]tbody[][tr[][td[][str"Careful"]td[][str<|character.Careful.ToString()]]tr[][td[][str"Clever"]td[][str<|character.Clever.ToString()]]tr[][td[][str"Flashy"]td[][str<|character.Flashy.ToString()]]tr[][td[][str"Forceful"]td[][str<|character.Forceful.ToString()]]tr[][td[][str"Quick"]td[][str<|character.Quick.ToString()]]tr[][td[][str"Sneaky"]td[][str<|character.Sneaky.ToString()]]]]]Column.column[][Heading.h2[][str"Other Aspects"]ul[][yield![foraincharacter.Aspects->li[][stra]]]Heading.h2[][str"Stunts"]ul[][yield![forsincharacter.Stunts->li[][strs]]]]]]leterrorViewmessage=[Notification.notification[Notification.ColorIsDanger][strmessage]]letprivateviewmodeldispatch=Container.container[][Content.content[][yield!loadingMessagemodelmatchmodel.Characterwith|Somec->yield!characterViewc|None->()matchmodel.ErrorMessagewith|Somem->yield!errorViewm|None->()]]openElmish.ReactopenElmish.DebugopenElmish.HMRProgram.mkPrograminitupdateview#ifDEBUG|>Program.withHMR#endif|>Program.withReactUnoptimized"elmish-app"#ifDEBUG|>Program.withDebugger#endif|>Program.run
And there you have it - a simple app that loads "Bob" from our server, using the generic fetchAs method to cast the JSON back into our strongly typed world. Making the application interactive and more attractive is left to the user; it gets quite addictive with a nice type safe wrapper over React and auto-reloading.