Dzoukr / SAFEr.Template

Strongly opinionated modification of amazing SAFE Stack Template for full-stack development in F#.
MIT License
107 stars 15 forks source link

template oddities (routing, index.html) maybe Fable.Remoting misconfig #22

Closed houstonhaynes closed 1 month ago

houstonhaynes commented 1 month ago

So I was pretty excited about this template - and still am - but have hit a few head-scratching issues that caused me to re-create a new project with a "vanilla" template and realizing there are some gaps in the assumptions in the boilerplate code.

First off - there's no index.html. So if you try to run the server from a cold start and go to the home path as the logs instruct you get a big nasty .NET error. Once I added a placeholder index.html that bit was taken care of.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Server Placeholder</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
            background-color: gray;
            color: white;
        }
        .container {
            text-align: center;
        }
        h1 {
            color: black;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Server is running</h1>
        <p>This is a placeholder page for the web server.</p>
    </div>
</body>
</html>

Secondly - and this is the interesting bit - when I go to the "GetMessage" endpoint - I also get the index.html page in return.

Even more interesting - if I put in a wrong endpoint I also get back the index.html page.

I don't know that much about Fable.Remoting so I just hand-jammed my way through to getting things to behave as I expect using this Program.fs

module CollabGateway.Server.Program

open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Giraffe

let private configureWeb (builder: WebApplicationBuilder) =
    builder.Services.AddGiraffe() |> ignore
    builder.Services.AddLogging() |> ignore
    builder

let private configureApp (app: WebApplication) =
    let getMessageHandler: HttpHandler =
        fun next ctx ->
            task {
                let response = "Hello, world!"
                return! json response next ctx
            }

    let notFoundHandler: HttpHandler =
        fun next ctx ->
            ctx.Response.StatusCode <- 404
            ctx.Response.WriteAsync("Not Found") |> ignore
            next ctx

    app.UseGiraffeErrorHandler |> ignore
    app.UseGiraffe (choose [
        route "/api/service/GetMessage" >=> getMessageHandler
        route "/" >=> htmlFile "public/index.html"
        notFoundHandler
    ])
    app.UseStaticFiles() |> ignore
    app

let private builderOptions = WebApplicationOptions(WebRootPath = "public")
let private builder =
    WebApplication.CreateBuilder(builderOptions)
    |> configureWeb

let app =
    builder.Build()
    |> configureApp

app.Run()

This gives me what I would expect from the boilerplate - a static index page for the root, a "Hello, World!" response from api/service/GetMessage and "Not Found" for everything else. I may eventually take a turn at Fable.Remoting to understand it better and perhaps tweak what's going on in this template.

But for now I just need a pair of forms and a bit of interaction to work so I'm going to keep hand-jamming in Program.fs for this particular project. It's a shame that I had to "throw out" Shared.API because I wanted to give it a try under these limited/controlled circumstances. So I expect to return to this at some point to deepen my understanding of the mechanism "a few layers down". In the meantime if there's interest in checking this out by someone who knows this "stack" better, I'd be eternally grateful. (even if it's just a documentation update to notify newbies to this template what to expect with server as-configured) Thanks!

Dzoukr commented 1 month ago

Hi @houstonhaynes,

I fully understand your confusion, but this is coming from the specifics of single page apps (SPA) vs classic API endpoint. Let me adreess is one by one:

First off - there's no index.html

There is. Located in Client project. If you run dr publish command, you will see the final output of the build, where index.html is correctly located. And this index.html is used for every response for SPA, because... that's how SPA works. If you go to let's say /about page, what actually happens is that:

  1. Server tries to find /about route
  2. It does not exist
  3. Server fallbacks to sending index.html instead
  4. Page is initialized including JavaScript
  5. Javascript finds "wow, someone tries to get to the about page" and routes you there on the client side (!!! important)

The same goes when you do the routing in SPA without full refresh - JavaScript just changes the URL in the browser + renders a different part of the SPA. That's where SINGLE-PAGE naming comes from. Confusing, yeah, but all went through the same horror.

Secondly - and this is the interesting bit - when I go to the "GetMessage" endpoint - I also get the index.html page in return.

Again, this is something that may slip through your attention when reading Remoting docs. For any server call where there may be some parameter coming to the server (like in /GetMessage), it uses POST (!!! important)

image

This POST normally returns JSON as you would expect.

I don't really recommend to getting rid of the Shared project, because it's actually the only reason I would go for SPA, Fable & Remoting. Without the shared part, the benefits of this templates are close to zero. 😅

I hope this helped and wish you happy F# coding! Glad to have you in community! 💪

Dzoukr commented 1 month ago

Btw, here you can see the production-ready app with routing, different parts, shared part, etc...

https://github.com/Dzoukr/FuncasterStudio

houstonhaynes commented 1 month ago

Thanks for taking the time to respond. I think we have tripped up on a slight miscommunication, which is caused a pretty significant Divergence and what I'm talking about and what your response includes.

I was strictly talking about server and how server was set up. There is a route that goes to public/index.html but there is no HTML generated in the template.

To be clear, I'm not getting rid of Shared at all - but I'm not importing it into server to use the service definition that's included there. I'm essentially building my own routing manually because the routing as included in the shared API does not seem to work.

Dzoukr commented 1 month ago

routing as included in the shared API does not seem to work

This is where I don't understand the issue. If you take what is in the template and copy-paste, you can have several APIs including routing for the frontend part. Maybe you are trying to use the Remoting library for REST API server consumed NOT from the SPA? Then of course you should create your own - this Shared is mainly for communication between frontend and backend.

There is a route that goes to public/index.html but there is no HTML generated in the template.

Yes, in development mode, the index.html is returned by Vite.js dev server. In production mode dr publish you will find it in the right place - public/index.html - and you can directly deploy like that to Docker, Azure, etc...

Dzoukr commented 1 month ago

Example

API definition in Shared: https://github.com/Dzoukr/FuncasterStudio/blob/master/src/FuncasterStudio.Shared/Episodes/API.fs Server implementation: https://github.com/Dzoukr/FuncasterStudio/blob/master/src/FuncasterStudio.Server/Episodes/API.fs Server registration: https://github.com/Dzoukr/FuncasterStudio/blob/master/src/FuncasterStudio.Server/WebApp.fs Client usage:

houstonhaynes commented 1 month ago

OK - this is a conceptual problem on my part. What I'm seeing here is that since there's not any RESTful endpoints to separately check the validity of the server deploying that essentially hard couples the front end and back end together. I'm not sure I'm a fan of that idea. But I'll go through the example see if I can get my head wrapped around the assumptions.

Dzoukr commented 1 month ago

It's not a problem on your part (you are too hard on yourself). It's just a different approach. You can easily set FAKE scripts to deploy FE or BE only, but when using Shared project, these things are tangled together anyway. For having RESTful endpoints, just see how Giraffe works and on the frontend side, you can use something like Thoth.Json to have it truly separated. But some SPA specifics like routing still remains. For choosing the correct communication, see the table below:

image

More info on that is on official SAFE template: https://safe-stack.github.io/docs/features/feature-clientserver/

Dzoukr commented 1 month ago

Maybe last remark: Fable.Remoting is a great library when "I don't care about HOW the communication looks like, Ï just need a strongly-typed safe data exchange between MY frontend and MY server." Once there are more potential consumers (other systems using your REST API), there Fable.Remoting is no-go (or use it only for you on some /internal-api/ path).

houstonhaynes commented 1 month ago

I hear you - and understand the natural tension there - and I've felt it. There's the desire to avoid/circumvent the standard pratfalls of "JSON all the thingz" in the HTTP/RESTful world. I've even dealt with the limits/burdens of that in the protobuf "sphere". Even there I have my own misgivings about whether the attempt at data guarantees 'over the wire' is a fool's errand (as it would create a level of coupling that would be "golden handcuffs" and a recipe for building up technical debt faster than it can be refactored in a changing business landscape). At that point it ceases to be a "stack" and is just another monolith, which I want to avoid.

I didn't even know about Elmish.Bridge until your mention of it above, so there's definitely a need for me to expand my survey of the landscape.

Such as it is - I'm still a bit mystified (and likely a blind spot on FAKE) by the prospect of deploying the Client to Cloudflare Pages and the Server to DigitalOcean and have them "talk to each other" without wiring things up "old school". I don't mind staying canonical with Fable.Remoting for this particular application because it's really just a sales lead pipeline management tool. BUT and there's always that but - the actual showcase app we're building is more 'hairy' and we do have ambitions to grant some degrees of freedom to where an Avalonia client (mobile/tablet/desktop, you get the idea) uses the same back end - hopefully with the same type safety assurances offered in a "canonical" SAFE stack.

That's probably more than I should try to address in the first go with this stack, but at least I wanted to paint the bigger picture in order to provide the broader goal(s).

Thanks again!

houstonhaynes commented 1 month ago

BTW - I've never seen Server deploy in anything other than "Production" mode (which limits logging output) even when deploying to localhost:5000 and a public/index.html on Server was never generated when deploying using 'dotnet run' as instructed in the guidance.

houstonhaynes commented 1 month ago

Got everything "cleaned up" and working with the standard Fable.Remoting doing the routing and it's all good. Thanks again for cribbing me through. I just have trust issues and ketp trying to hand-roll everything when it's really not needed in this case. I'll save that for where it's warranted. ⭐