Fork me on GitHub
#reitit
<
2022-11-22
>
Tema Nomad15:11:56

How should I separate reitit routes when develop SPA with frontend/backend separation? The problem is: IF I put all /api requests to backend handler AND put every other requests to frontend html-page (with compiled js via shadow-cljs) AND I have some routes on frontend (via reitit.frontend) THEN (from @vale) > this does have the negative effect that your server returns 200 ok, even for routes that end up with a "page missing" error.

Tema Nomad05:11:07

I found this docs https://github.com/metosin/reitit/blob/master/doc/advanced/shared_routes.md But I dont understand it. So first part "Using reader conditionals" is about to use 1 shared .cljc file for routes and separate them via clojure reader conditionals #? But what is about second part "Using custom expander"? In docs example, first we create shared .cljc routes, then use it as is in frontend routes, but then dont use it at all in backend routes (I dont see any routes using there). Why? In this example we just re-write all routes in backend part

valerauko05:11:59

the :expand option is in reitit.core meaning you can use it the same from frontend and backend. i think what you noticed is just a mistake in the docs and you'd use the routes defined in cljc on the server as well

valerauko05:11:43

this method also has the advantage that you can generate urls using an uniform route naming scheme both on backend and frontend which is pretty awesome

Tema Nomad05:11:21

> i think what you noticed is just a mistake in the docs yeah probably, will try to use it > this method also has the advantage what do you mean?

valerauko05:11:53

if you have a common routes.cljc with routes like in the example, you can always be sure that reitit's match-by-name will always return the same route for the same name. no worrying about having to maintain the same names separately on the frontend and on the backend

Tema Nomad05:11:51

aaaah I see. And on backend :expand part I just use that keyword names for api handlers?

Tema Nomad05:11:43

but how is about custom options on frontend? Like :view ?

valerauko06:11:33

as i said before i haven't used this method myself so please experiment haha i get the impression that your :expand function takes the current "value" for the route -- in that example, ::kikka and returns the appropriate options for that route. in a backend ring router that'd be like in the example. on the frontend it'd return a map with :view or :controllers according to your needs

Tema Nomad07:11:22

Now I have to double all frontend routes in backend :expand part and anyway it doesnt work (links are not working)

valerauko07:11:47

you need to provide more information than "anyway it doesnt work" when asking for help

Tema Nomad09:11:08

but I dont know is it proper way or not For example, I did not use ::keyword as routes name keyword, I used a common :keyword (because if I use ::keyword I need to write full namespace chain like :foobar.shared.routes/index instead)

Tema Nomad10:11:30

And main issue/thought - I still need to put all frontend routes at my backend routes config (see https://github.com/temanmd/clojure_learning_foobar/blob/main/src/foobar/backend/routes.clj)

Tema Nomad10:11:11

So I dont see any advantage to use reitit shared routes still

juhoteperi10:11:08

Reitit frontend also supports fragment routing, which is the default even. With fragment routing the backend doesn't see the frontend routes and only needs to provide the index route.

👍 1
juhoteperi10:11:23

No, I mean the frontend urls look like /#/frontend/route

juhoteperi10:11:15

The backend doesn't see the path after # so from the view of the backend, user is always requesting just the index file

Tema Nomad10:11:47

but it not a good solution from user POV and from search engines POV

juhoteperi10:11:25

I don't see why a user would care what the url looks like. If you are building a site that is indexed, sure, that is a valid reason.

Tema Nomad10:11:27

It just looks more clean and humanized 🙂

juhoteperi10:11:05

I think at least previously for example Node.js + React projects just returned the index.html from not-found handler. Frontend will then check if the current route is found on the frontend route tree, and can display not-found error if the route isn't found there either.

juhoteperi10:11:31

Or you can indeed share the frontend route tree to the backend as cljc, and then check if the path exist there before serving index.html

Tema Nomad10:11:32

So I have shared cljc routes

juhoteperi10:11:48

I think you could either place the frontend tree under backend route tree and replace/add index.html handler for each frontend route, or implement not-found handler which manually checks the frontend tree with match-by-path and returns index.html if a route was matched, or the 404 otherwise.

👍 1
Tema Nomad10:11:56

but dont understand why I have it 😄 if I need to describe it again at backend

valerauko10:11:34

you can use a case instead of a literal map

(case route-name
  ::api/whatever api/whatever-handler
  ::api/something api/something-handler
  ;; else
  return-index-html)

Tema Nomad10:11:15

@vale but in this case I still cant return HTTP 404

Tema Nomad10:11:27

because it will be at index-html already

valerauko10:11:44

no, it won't. it only expands routes you have defined

juhoteperi10:11:16

You shouldn't need duplicate frontend route trees

juhoteperi10:11:24

The view component references aren't available, but you don't need them in backend, so you could just wrap them in #?(:cljs home-page)

valerauko10:11:53

you can expand on the frontend too

Tema Nomad10:11:43

So I use :expand on both sides

valerauko10:11:33

ah indeed. i saw the myroutes def and didn't notice you never used it

Tema Nomad10:11:46

ah sorry I have to remove it)

Tema Nomad10:11:25

hm, I will try to use "" as a general starting point for all frontend routes

juhoteperi10:11:47

If your frontend routes include parameter validation etc, those might be unncessary on the backend. For backend to serve the index.html it is probably enough that the path matches, and the frontend can handle the parameter validation.

valerauko10:11:37

(defn my-expand
  [data opts]
  (if (keyword? data)
    (case data
      :api api-handler
      ;; else
      frontend-handler)
    (ring-core/expand data opts)))

{:expand my-expand}

Tema Nomad10:11:39

Now I only pass handler on backend for all frontend routes. And other options/validations are on frontend part already (via :expand)

juhoteperi10:11:25

I might write the parameter declarations to the shared file still, to keep most of the frontend route tree in one place, and then just toggle the validation off when using those routes on the backend. But either way works.

valerauko10:11:29

yeah the expand takes all the route data so you can look at the (:name data) if you decide to use maps

Tema Nomad10:11:00

but this solution ruins "HTTP 404" return

valerauko10:11:48

how so? did you actually try it?

Tema Nomad11:11:51

Don't know how to create ISeq from: foobar.backend.routes$frontend_handler

Tema Nomad11:11:13

Also had a error:

Don't know how to create ISeq from: foobar.backend.routes$my_expand$fn__2780
if I use expand func like this:
(defn my-expand [data opts]
  (fn [data opts]
    (if (keyword? data)
      (case data
        :api api-handler
        frontend-handler)
      (ring-core/expand data opts))))

Tema Nomad11:11:08

It WORKS!!!!!!!! 🥳

Tema Nomad12:11:27

because it should return {:handler handler-func} format, not just a handler

valerauko12:11:42

i'm glad you got it to work

Tema Nomad12:11:53

Thank you man!

Omar13:11:43

I'm a bit confused why you'd want to do all that, this is what my routes look like in one of my projects:

;; frontend-routes cljc file

(defn routes []
  [[""
    #?(:clj {:get layout/public-page}) ;; <-- line to not having missing page on any routes
    ["/"
     {:name        :home
      :view        :home
      :controllers [{:start (fn [_]
                              #?(:cljs
                                 (rf/dispatch [:page/init-home])))}]}]
    ["/requests"
     [""
      {:name :requests
       :view :requests}]
   rest of routes ...

;; handler.clj

(mount/defstate app-routes
  :start
  (ring/ring-handler
   (ring/router
    [(frontend/routes)
     (api/routes)])
   (ring/routes
    (ring/create-resource-handler
     {:path "/"})
    (ring/create-default-handler
     {:not-found
      (constantly (error-page {:status 404, :title "404 - Page not found"}))
      :method-not-allowed
      (constantly (error-page {:status 405, :title "405 - Not allowed"}))
      :not-acceptable
      (constantly (error-page {:status 406, :title "406 - Not acceptable"}))}))))

Tema Nomad16:11:33

Your example works good too Look at my example repo. I dont use #?(:cljs), I just mark routes names in my cljc file and then use :expand to separate front/back end routes

Tema Nomad16:11:22

I mean in your example you mix frontend routes functionality (view, controller, dispatch) inside shared cljc file My solutions is more clean because I describe frontend part only in frontend files cljs and leave only plain vector in cljc