Commonly frontend and backend are separate beasts. Backend written in Ruby using Rails for example, its routing written in its own DSL. Frontend written in TypeScript using Vue.js for example, its routing written in its own DSL. Of course the frontend will call some backend endpoints, so it should definitely know about those backend endpoints too, while there may be some frontend “paths” that don’t correspond to any single API endpoint, yet you might want to generate absolute URLs for those pages on the backend. This results in a nasty mess and duplication of routing and adjacent logic.

asphalt road between trees

One of the big selling points of Clojure and ClojureScript is that you get to write the web frontend and backend in (pretty much) the same language. Of course the requirements and so the libraries used are very different (running a HTTP server vs rendering HTML in the browser, fetching data from a persistent database vs getting stuff from some API over HTTP), but routing is a common concern. Using the routing library reitit, it’s possible to use the same route definitions in the browser and the server. The way you write the routes needs to be adjusted a bit, but I think it’s absolutely worth it.

The first step is to have the route definitions (the vectors like ["/" ::home]) in a .cljc file so that both serverside Clojure and frontend ClojureScript can use them. At first just have path-name pairs like above to keep it simple. There are two ways to get the CLJ/CLJS difference in the routes. Either use reader conditionals or the :expand option for reitit.

I’m not a fan of reader conditionals, because they can get really nasty really quick and it’s hard to follow what’s going on. It’s an option nonetheless. I think a good usecase for the reader conditional way is adding common middleware for whole parts of the route tree (expanding, which deals with individual endpoints, isn’t really a good fit to deal with that).

The :expand option accepts a function that turns whatever the route definition is in the code, and “expands” it to a full route definition. I think that reading the default expand‘s implementation is a good starting point for that. The expansion logic can be just about anything, but I’d assume the common thing is to have a route-name to route-definition mapping somewhere, either in a literal map or a case.

aerial photography of concrete roads

While the document skids over it, route expansion is a reitit.core feature, so it’s available both in the frontend and the backend. The underlying implementation is the same (minus reader conditionals). This also means that there can be even a shared Expand protocol (or multimethod) with separate implementations for the backend and the frontend, though I couldn’t get this working in time.

The problem with sharing routes between frontend and backend isn’t even the route definitions themselves, but the functions (middleware, handlers etc) used in those definitions. As long as the symbols resolve to something meaningful in the execution context (like the :handler on the ring backend being a function), the routes can be shared just fine. As reitit is very flexible and lets you put whatever you want in route definitions, unknown things will just live under :data as luggage, but otherwise not cause any trouble. (More on how that can be used later.)