It has been quite a while (once again hah!) while I've been busy working in a few of my own F# projects the truth is that I didn't have much to write about in F# after my last "Simple things in F#" series.
Today I don't have a new series to start with but rather a simple example which may or may not grow in another blog series. For the moment we'll talk about how to do File uploads to an F# backend powered by Falco and Saturn so let's get started!
The samples for this post can be found at this repository:
While they're mainly the same as they both are built on top of ASP.NET it is nice to have a focused sample that works as a reference in case you need it.
You just found F# or saw a tweet/toot/thread related to F# and decided to give it a go, you've been told there are F# web frameworks like falco and saturn but you haven't seen a lot of samples out there, it would be nice if there's a focused sample you can take a look at.
Well let me tell you that I've been there as well! Well... in my case it was with php, then with node, then with C# and at some point F# as well hah! But I digress, Let's start with our client side where we'll be sending files to our back-end.
HTML Forms
At this point in time, HTML is present everywhere so this is where we will start, given the following HTML:
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>Send a File to the Server</title></head><body><!-- Standard HTML Post --><formid="standard"><fieldset><legend>Standard Html Upload</legend><!-- don't forget the name, it is quite important! --><inputtype="file"name="my-uploaded-file"/><!-- trigger the submit event with this button/input --><inputtype="submit"value="Upload"/><progressid="standard-progress"style="opacity: 0"></progress></fieldset></form><!-- Once the server answers our request we'll set the responses below --><sectionid="response-target"></section><sectionid="error-target"></section><!-- Let's also sprinkle vanilla JS here to make it a little bit dynamic --><script>// Let us wait for the DOM to load, so we can find our HTML nodesdocument.addEventListener("DOMContentLoaded",()=>{// let's gather the elements we'll be working withconstform=document.querySelector("#standard");consttarget=document.querySelector("#response-target");consterrorTarget=document.querySelector("#error-target");constprogress=document.querySelector("#standard-progress");form.addEventListener("submit",(e)=>{// avoid page reloads we'll send our request manuallye.preventDefault();// these days building a form data is so simple// just pass the form reference to the constructor and// all inputs related to the form will be added automaticallyconstformData=newFormData(form);progress.style.opacity=1;// We'll use a standard fetch here for simplicityfetch("/uploads",{method:"POST",body:formData,}).then((response)=>// if the status code is not ok, then we'll send the text to the catch handlerresponse.ok?response.text():// remember text is a promise, so we'll need to extract that thereresponse.text().then((content)=>Promise.reject(content))).then((response)=>{// if all goes well then we'll swap our target's html// with the server's responseconsole.log("Success");target.innerHTML=response;}).catch((error)=>{// you can reject promises with anything not just Errors so we'll checkif (errorinstanceofError){errorTarget.textContent=error.message;return;}// if it was not an error then it is likely we're here due to our// handler aboveerrorTarget.innerHTML=error;}).finally(()=>{// hide the progress in any caseprogress.style.opacity=0;});});});</script></body></html>
Our client side HTML is as simple as we can get it, just a form and a few lines of JavaScript to avoid a page reload. The form submission will hit the /uploads endpoint in our server, this index file will be server by our server as well so no need to point to the full address (also to leave CORS out of the situation for the moment).
Saturn Back-end
Fortunately our sample is so focused that it can be implemented in a file with around... 50-60 LoC
openSystemopenSystem.IOopenMicrosoft.AspNetCore.HttpopenSaturnopenGiraffeopenGiraffe.ViewEngine// This is a templating function to produce HTML content that we'll use later onletresponseTemplatecolorcontent=// Giraffe.ViewEngine uses a tagElement attribute list tagElement listarticle[_style$"color: %s{color}"][header[][// encodedText is a function that produces a string with encoded html,// this avoids sending raw html to the client from the serverh3[][encodedText"File Contents below:"]]pre[][encodedTextcontent]]// the index handler is quite simple, just send the html file which is located by// the relative path to the current working directory of the serverletindexnextcontext=htmlFile"./index.html"nextcontext// This is our File Uploads Handlerlethandlernext(context:HttpContext)=task{// Saturn/Giraffe can also use aspnet's features directlyletform=context.Request.Form// From the ASPNET's form abstraction we try to get a single file with the name// we provided, we also convert it to an option to handle potential null valuesletextractedFile=form.Files.GetFile"my-uploaded-file"|>Option.ofObj// if we wanted to extract multiple files we'd use something like form.Files.GetFiles "input's name attribute"matchextractedFilewith|Somefile->// if the file is present in the request, then we can do anything we want here// from validating size, extension, content type, etc., etc.// For our use case we'll create a disposable stream reader to get the text content of the fileusereader=newStreamReader(file.OpenReadStream())// in our simple use case we'll just read the content into a single stringlet!content=reader.ReadToEndAsync()// we'll write the file to disk just as a sample// we could upload it to S3, Google Buckets, Azure Storage as welldo!File.WriteAllTextAsync($"./{Guid.NewGuid()}.txt",content)// We received a file and we've "processed it" successfullyletcontent=responseTemplate"green"content// send our HTML content to the client and that's itreturn!htmlViewcontentnextcontext|None->// The file was not found in the request well return an error messageletcontent=responseTemplate"tomato""The file was not provided"// and also set our status code to 400 to signal it was a client errorreturn!(setStatusCode400>>htmlViewcontent)nextcontext}// lastly register our routes in the routerletappRouter=router{get"/"indexget"/index.html"indexpost"/uploads"handler}// create a serverletserver=application{use_routerappRouter}// run it baby!runserver
That's it! Nothing too crazy I hope!
Let's put that into an F# project
# create the project
dotnet new console -lang F# -o saturn-sample
cd saturn-sample
# add the Saturn package
dotnet add package Saturn
# create the empty index filetouch index.html
Now copy and paste the contents of the previously provided client side HTML into the index.html file, then copy F# source code and replace the contents of the Program.fs file.
and start your app.
dotnet run
You should be able to visit http://localhost:5000/ and start testing the file upload.
Falco Back-End
Fortunately, the falco sample is not much different and also fits in a single file! we'll be using the same index.html that we shown above so we'll go straight to the F# source code for this.
openSystemopenSystem.IOopenSystem.Threading.TasksopenMicrosoft.AspNetCore.HttpopenFalcoopenFalco.RoutingopenFalco.HostBuilderopenFalco.Markup// Similarly to the saturn sample// we'll provide a templating function// this one uses Falco.Markup though rather than Giraffe.ViewEngineletresponseTemplatecolorcontent=Elem.article[Attr.style$"color: %s{color}"][Elem.header[][Elem.h3[][Text.enc"File Contents below:"]]Elem.pre[][Text.enccontent]]// equally to the previous sample we'll read the index file and return an html responseletindex(context:HttpContext):Task=task{// the only difference here is that we are able to pass the request aborted cancellation tokenlet!content=File.ReadAllTextAsync("./index.html",context.RequestAborted)return!Response.ofHtmlStringcontentcontext}lethandlercontext:Task=task{// Falco can also use aspnet's features directly// but offers an F# API for ease of uselet!form=Request.streamFormcontextletextractedFile=// extract the file safely from the// IFormFileCollection in the http contextform.Files|>Option.bind(funform->// try to extract the uploaded file named after the "name" attribute in html// GetFile returns null if no file is present, so we safely convert it into an optional valueform.GetFile"my-uploaded-file"|>Option.ofObj)matchextractedFilewith|Somefile->// if the file is present in the request, then we can do anything we want here// from validating size, extension, content type, etc., etc.// For our use case we'll create a disposable stream reader to get the text content of the fileusereader=newStreamReader(file.OpenReadStream())// in our simple use case we'll just read the content into a single stringlet!content=reader.ReadToEndAsync()// we'll write the file to disk just as a sample// we could upload it to S3, Google Buckets, Azure Storage as welldo!File.WriteAllTextAsync($"./{Guid.NewGuid()}.txt",content)// We received a file and we've "processed it" successfullyletcontent=responseTemplate"green"content// send our HTML content to the client and that's itreturn!Response.ofHtmlcontentcontext|None->// The file was not found in the request return somethingletcontent=responseTemplate"tomato""The file was not provided"return!context|>Response.withStatusCode400|>Response.ofHtmlcontent}// declare the host and the endpointswebHost[||]{endpoints[// handle the index routes as wellget"/"indexget"/index.html"index// our file endpoint handler as wellpost"/uploads"handler]}
Again, let's put that into an F# project
# create the project
dotnet new console -lang F# -o falco-sample
cd falco-sample
# add the Falco package
dotnet add package Falco
# create the empty index filetouch index.html
Now copy and paste the contents of the previously provided client side HTML into the index.html file, then copy F# source code and replace the contents of the Program.fs file.
and start your app.
dotnet run
You should be able to visit http://localhost:5000/ and start testing the file upload.
Both backends in F# are virtually the same a few differences here and there might make it more appealing for some than others but the premise is the same, using F# should make it easy and safe regardless of your preferred style and we can see this in using the F# Optional type to avoid null reference exceptions which can bite hard in languages where null is the day to day bread.
Please note the usage of the use keyword in the samples, for disposable references (the ones that implement the IDisposable interface) F# can make the disposition automatically at the end of the scope when this keyword is used, otherwise you would need to manually call reference.Dispose() which can be easily forgotten so... avoid that and just let F# handle it for you!
I must mention that when you're dealing with file uploads you should avoid reading the contents into a string, this is for performance reasons mainly because rarely you actually need to inspect the contents of the file you usually pass it on to another file upload service like I mentioned in the sample, aws, gcloud, azure, you name it letting the code process a stream is better as you don't allocate the string in memory for un-needed reasons.
Bonus HTMX
You may have heard of this HTMX thing... that supposedly makes it easier to deal with client side HTML + any existing back-end.
Well, let me tell you that it works wonders with F# as well!
to use HTMX we'll change our HTML a little bit:
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>Send a File to the Server</title><!-- Include HTMX in your website --><script src="https://unpkg.com/htmx.org@1.9.3"></script></head><body><!-- HTMX uses hx-* attributes to indicate the behavior of the element that will send
an http request, in this case
we're telling htmx to post a form-data request to the /uploads endpoint
and when the server sends a response replace the inner html of the response target
with the server's response
--><formid="htmx-form"hx-encoding="multipart/form-data"hx-post="/uploads"hx-target="#response-target"hx-swap="innerHtml"><fieldset><legend>HTMX Upload</legend><inputtype="file"name="my-uploaded-file"/><button>Upload</button><progressid="htmx-progress"value="0"max="100"></progress></fieldset></form><sectionid="response-target"></section><sectionid="error-target"></section><!-- Closely to vanilla JS, but with a smoother API to work with --><script>// given the element (specified by the selector) listen to the progress event in the// backing hxr request// crafting an xhr request manually is quite lengthy so that's why we used fetch in the plain// html example, htmx thankfully hides that for us and lets us focus on the omportant bitshtmx.on("#htmx-form","htmx:xhr:progress",function (evt){htmx.find("#htmx-progress").setAttribute("value",(evt.detail.loaded/evt.detail.total)*100);});// if the backend sends an error we'll replace the contents of the error target insteadhtmx.on("htmx:responseError",(error)=>{evt.detail.target=htmx.find("#error-target");});</script></body></html>
You can replace the contents of the index.html file with that and it should work just as great, with the difference that we need to write a little bit of JS ourselves and we also get file upload progress as well!
Closing thoughts
Hopefully this shows that F# is not an alien language and it is more similar than you think, no weird maths, no weird point free code. This is very close to what you would write in other languages like typescript given that they provide a framework similar to aspnet!
Does this help you at all?
Do you want more content like this?