https://www.evalapply.org/posts/clojure-web-app-from-scratch/index.html Aditya Athalye writes and works here. Aditya Athalye writes and works here. read see mail l self >> now [?] feed Cooking right now: "Project Writing for nerds". Clojuring the web application stack: Meditation One [ | toc ] Published: 2024-08-24 Updated: 2025-02-18 By: Aditya Athalye In a land bereft of a canonical "killer app" web framework or two, one must think about the what, why, how, where of all the moving parts. Out here, one must become a student of web framework architecture in addition to web application architecture. For here, in Clojure-land, the two are one. --------------------------------------------------------------------- email me comments >> share - [bsky] [in] [HN] [lobste.rs] [reddit] feed --------------------------------------------------------------------- [ ] Email [Get posts by email] --------------------------------------------------------------------- Tags: #clojure#web_development#websites#functional_programming# software_design#architecture#howto#whyto --------------------------------------------------------------------- Contents Getting about (this post) Getting Parable-ic Getting the Big Picture Getting Philosophical What does a Killer App kill? What's the catch? In Clojureland you stack libraries and the odds yourself The Ring world Framework-like web stack projects Dependency injection for those in the know That's not all folks "There is no [DEL:spoon:DEL] architecture" Why become a student of the web stack? Getting First-Principled A web app is just a polymorphic dispatcher Ring with Jetty is the classic combo Bare minimum ring.adapter.jetty web app Bare minimum directory structure Bare minimum library dependencies Bare minimum code Bare minimum live application Bare minimum HTTP requests Bare minimum Ring project derived from first principles Interface with the outside world Interface with us Provide HTTP creature comforts Orchestrate and control handler execution using middleware Bare minimum router - the polymorphic dispatcher appears Getting Started and Tutored Speed run through small demos Do more hands-on practice Review the current state of the art Putting one's neck on the line Footnotes --------------------------------------------------------------------- Getting about (this post) * This post is big! Skip whatever bores... Follow the nice ToC! * It is an "I to I" explanation I wish I had long ago. I've referenced getting started material and "batteries included" Clojure web stacks toward the end, which may be the most practically useful section of this post. + Update (2025-02-18): Links to my talk at Functional Conf 2025: Composing (Clojure) Web Stacks using Functional First Principles (video, slides). + Update (2024-09-04): I just published a complete project that follows up the concepts set up in this post. adityaathalye/ usermanager-first-principles is a stripped-down variant of seancorfield/usermanager-example. The project README explains all. * Errors and inaccuracies are all mine ^1. If you spot any, please write to complaints @ my domain. (And if you want to say nice things, write to compliments @ my domain :). Feel free to discuss on HackerNews (here) and r/Clojure (here). * A basic grounding in the Clojure programming language is assumed ^2. Familiarity with web development will help. * I will stick to discussing the Clojure web application stack in relation to classical Web Frameworks. Primarily ^3. * Yes, a Meditation Two is in draft hell. It is about Getting Pretty Deep In The Woods. If it sees the light of day, it will also be a giant post. Lambda help us. Getting Parable-ic Multitudes of sworn "Rails developer"s, "Laravel developer"s, "Django developer"s, "Next.js developer"s and suchlike throng the universe... Why? Novices don't know they don't know. The Framework saves them from themselves. So they survive. Intermediates know enough but would rather not have to. But it is a living. A person must eat. So that's that. RTFM. Experts know enough to care deeply ^4. The Framework is muscle memory; all the hacks, the tricks, the deep dark secrets. It bends to will. Mostly. Masters know enough to not roll their own. And yet... sometimes they do. A new cycle begins. Once upon a time, there was one. WebObjects. Now they are numberless. Getting the Big Picture Picture the Clojure web stack this way... to a good first approximation: * Our business logic (written in Clojure), + relies on a bunch of Clojure libraries (frequently, ring-clojure), o that know how to use an application server (Jetty). * In the simplest deployment model, this picture fits within a single compute instance (e.g. a PC, Cloud VM, or "Serverless" container). | | | The bulk of our | Our Business Domain's | application logic, | data representations | written in Clojure. | {} [] #{} '() 'x 42 | | +- - - - - - - - - - - + -- RING SPEC -- -- -- -- -- -- | | ^ | | The subset of Ring | | | | libraries we use | { } REQUEST hash-maps | as-provided, and | | | | as utilities to | | | | make custom handlers| | | | and middleware in | | { } RESPONSE hash-maps | Clojure. | | | | | | v +----------------------+ -- RING SPEC -- -- -- -- -- -- | CLOJURE MAPS | Clojure facing interfaces | | (functions, hash-maps) +- ring.adapter.jetty -+- - - - - - - - - - - - - - - - | | Java facing interfaces | JAVA OBJECTS | (Servlet API, Jetty config API) +----------------------+ ------------------------------ | Jetty | (deserialize ^) | (Application server, | HttpServlet Objects | "embedded" mode.) | (serialize v) +----------------------+ ------------------------------ (Plaintext HTTP Responses v) NETWORK BOUNDARY facing the WWW side (Plaintext HTTP Requests ^) +----------------------+ ------------------------------ | Public Web Server | (deserialize ^) | (for SSL termination | HTTP Objects | static assets etc.) | (encrypt, serialize v) +----------------------+ ------------------------------ (HTTPS Responses v) NETWORK BOUNDARY with the Public WWW (HTTPS Requests ^) But before getting too practical, an indulgent philosophical interlude. Getting Philosophical I think frameworks are a form of industrial automation (of choices, behaviour, workflows, detail and so forth). Perceived this way, they embody the tradeoffs of industrial automation ^5. Tim Ewald makes astute observations about this phenomenon, in his talk "Clojure: Programming with Hand Tools". As he remarks, pervasive use of automation has the insidious quality of changing how we view the world and how we perceive problems. To a mind invested in a framework, all web software will look irresistibly framework shaped. Squint just right, and the answer reveals itself. And they may very well be right. Until they are not. The Clojure world does it the hard way; viz. the not-framework way. What does a Killer App kill? Building web applications is arguably the most well trodden path into the software industry. Naturally. Many of the most valuable companies on Earth are web applications. Well-heeled web forms dominate the world. As the Web evolved, the Web Application Framework gained status as the "killer app" of any respectable programming language ecosystem. Framework makers work hard to serve the multi-faceted, ever-evolving demands on the Web Application. Their products contain time tested ideas; accumulated knowledge of many minds, battle scars from full contact Kumite with the Wild Wild Web. In polite society, we call these scars "design patterns". Knowing a framework well can liberate a person ^6 from the rabbit holes of composing software to solve for things like: * App architecture (MVC) and code layout (project templates) * HTTP request/response parsing and handling * Routing and dispatch * HTML templating and/or rendering * API design and use (HTML / text / JSON etc.) * Form handling * Data serialisation / deserialisation * Sessions * Persistent connections (websockets, long polling) * Database connector / driver (e.g. JDBC) * Database ORM * Sending emails * Managing job queues * Configuration (via. environment variables, files, remote sources) * App runtime lifecycle (dependency injection, starting/stopping etc.) * Security (encryption, data sanitisation etc.) * Authentication and/or Authorization * Application logging * Monitoring (with metrics and/or probes to monitor the live runtime) * Building and Packaging * Deployment (new-age frameworks) * Boilerplate and glue code required to make all these work together. * Developer Experience (framework-aware tools and IDEs are life savers). * More... That said, as with all things, TANSTAAFL. What's the catch? Tradeoffs of using a framework stem from the degree of control ceded to it and its ecosystem (ideas, plugins, packages, tools etc.). One accepts a form of vendor lock-in, in lieu of anticipated benefits. Some tradeoffs are: * Fixed core architecture. Any framework's architecture is fixed; unchangeable from the outside. e.g. If you don't like the router or ORM or template engine built into your chosen framework, can you just rip them out and put in other choices (for API or performance or security reasons)? No, you would have to migrate to an entire alternate framework, and bet that this one will fulfill all your current and (unknown) future requirements. * Leaky abstractions. The design choices and mental models of the framework and/or plugin authors inevitably flow into the app. It comes to rely on how they encoded explicit and implicit behaviours, software design patterns, opinions about deployment and operations etc. The more one uses, the stronger it binds. * Upkeep. You own the design and upkeep of the whole composite, especially your self curated and/or bespoke parts that patch, adapt, or work around those leaky abstractions. Unavoidable framework updates are par for the course (e.g. security patches and/or access to new functionality). Even with no custom parts, app makers must carefully update all off-the-shelf plugins and tools to remain API - compatible. And then also update their own application code to be compatible with any updated third-party API. * Production expertise. Debugging production issues can rather quickly become about grokking the inner workings of the framework. Meaning, sooner or later one must become a student of that specific web framework. * Choices are an expert matter. Though popular language ecosystems have a canonical web framework or two, all have a plethora of alternatives. Choosing between frameworks is an expert matter. Newcomers are directed to the most canonical one for good reason. Each alternative embodies a concrete set of tradeoffs, community support, lore and so forth, all opaque to the newbie, and difficult to parse even for an expert outsider. This is perhaps why teams get built around a framework and one or two framework experts. In Clojureland you stack libraries and the odds yourself The culture here strongly prefers libraries over frameworks. Here is a quick overview of what we have in our web ecosystem, and the implications thereof. The Ring world The Ring project, by James Reeves (a.k.a. weavejester), is the Clojure ecosystem's canonical collection of HTTP libraries. Its design choices have a far-reaching effect on the whole Clojure web ecosystem. So it's worth becoming familiar with Ring. James also created hiccup (HTML rendering) and compojure (routing), which used with ring and Clojure's standard library are enough to create a functional traditional multi-page web application, backed by the file system. To use a database, all we need is a library like next-jdbc. And making a "modern-feeling" web UI has become easy with HTMX, which "just works" with hiccup. IMO, most web apps can start this way (and can probably stay this way). Framework-like web stack projects Several framework-like web stacks also grace Clojureland, viz. Fulcro , Biffweb, Kit (successor to Luminus), Duct, Pedestal etc. However, unlike object oriented frameworks that are fully integrated monolithic systems, these are open-ended sets of libraries that represent the project developer's opinion of how to build web applications. Newer projects like sitefox and donut aim to be more "fully integrated" frameworks. Single Page Application enjoyers may find hoplon cool. And if you want truly novel systems, check out hyperfiddle/electric, and Rama by Red Planet Labs. Dependency injection for those in the know Another approach is to use something like a dependency injection framework to connect and orchestrate all our app's moving parts through some common system. Libraries like component, integrant, mount, donut-system serve this purpose. These are favoured by people who already have specific opinions about what set of libraries and pieces of infrastructure they need (and why). That's not all folks We haven't even begun to enumerate a constellation of other libraries needed for databases, caches, security, logging, monitoring, queues, jobs and so forth. Even otherwise seasoned programmers, who are new to Clojure, can struggle to find their bearings amid this dizzying array of choices. "There is no [DEL:spoon:DEL] architecture" Alas, not only is there no obvious One True Framework, there is no obvious One True Framework Architecture either. This adds to every Clojure newcomer's struggle, even grizzled web veterans. As a thought experiment, I feel a Rails developer will find it easy to make sense of a Django or Laravel project, versus any of the apps built with tools we have in the Clojure ecosystem. Popular web frameworks, going all the way back to WebObjects (1996) are object oriented GUI software; products of convergent evolution along common industry-wide OOP patterns. They are designed for use via Public APIs. Core parts are welded together. Thus, a competent Rails developer parachuting into a Django project can reasonably expect to follow their nose down familiar-feeling Class hierarchies and method chains, across familiar Model, View, Controller structures. Why become a student of the web stack? The Clojure world, though built with Java for the JVM, .Net for the .Net CLR, and Javascript for node and browser engines, departs wildly from those underlying Object Oriented foundations. This fact deeply influences everything, including making web apps. So, although freshly-minted intrepid Clojurians will do well to pick the Ring stack, or one of the popular "starter kits", we must consciously become students of web framework architecture too. For out here, the problem of making a web application is also the meta-problem of composing a bespoke web stack. Getting First-Principled Many wonderful resources teach Clojure/ClojureScript web development. However, I struggled to build a coherent picture, until I worked out a first-principles model, upon which to build my understanding. So here are the bare essentials, to motivate further learning, using material I reference later. A web app is just a polymorphic dispatcher Think... what does a web app reeeeeally do? HTTP request -> /pattern-1/ method-1 /pattern-2/ method-2 /pattern-3/ method-3 -> HTTP response Shell scripting enjoyers will immediately think of AWK programs, and their design sense would not be wrong. But there is more to the story, of course. For a "pattern" is a set of one or more pieces of information culled from HTTP requests, most crucially the URI and the HTTP verb. ^7 HTTP request -> GET /uri-1/ getter-method PUT /uri-1/ putter-method POST /uri-1/ poster-method PATCH /uri-1/ patcher-method DELETE /uri-1/ deleter-method -> HTTP response This pattern tempts us to construct an HttpObject, and is arguably why modern-day OOP style appears to be a natural fit. Yes, the tiniest piece looks like an Object. And yes, the whole web app as a system is very Object Oriented. However, IMHO, the monolithic design of frameworks is rooted in having to use the smallest datum as some concrete HttpObject, instead of generic data. Clojurists favour generic data over concrete objects and composition over inheritance, because building with composable parts gives us almost unlimited control over the shape, size, and sophistication of our application. The initial learning curve pays off over time, as we get to keep simple apps dead simple, and to ensure not-so-simple apps are only as complex as they need to be. We build our polymorphic systems using Functional Programming parts. With this in mind, we construct the core intuition of the anatomy of Clojure web apps, which lives in the heart of ring-clojure... Ring with Jetty is the classic combo Refer back to the Big Picture. The Ring project is a crowd favourite for production Clojure ^8 web apps. It established the Ring specification along with the request / response handling model that many other Clojure web libraries support, or complement. Jetty is a popular server of choice in the Clojure community. It is generally used as an "Application Server" in "embedded" mode, i.e. we put the server inside our web application, as a regular library dependency. We can alternately invert the model and run Jetty in "standalone" mode, as a "container" runtime, i.e. we put our application inside the Jetty server. We will briefly peek at the Servlet API in a later section, as that is the common base for both modes of operation. But this post assumes we run our app in the community-preferred way. By and large, Clojurians prefer the embedded jetty way over the servlet container way ^9. Bare minimum ring.adapter.jetty web app Now we make a bare-minimum web app where the handler function is a catch-all method. It will return a string containing the request information for any HTTP request made to any route. This seemingly pointless code is actually useful to check that your project is set up right. Use it as a starter template. Bare minimum directory structure $ tree . # root of our project directory . +-- deps.edn # project configuration +-- classes # target for compiled code +-- src +-- first_principles +-- core.clj # our bare minimum app Bare minimum library dependencies Our deps.edn file contains this configuration; only Clojure and the Jetty adapter library from the Ring project. We use the Jetty adapter as-provided, to avoid rewriting a whole bunch of code to do Java interop and implement the Ring specification. We rely on these as standards, so we can assume they are available as a given. {:paths ["src" "classes"] :deps {org.clojure/clojure {:mvn/version "1.11.3"} ring/ring-jetty-adapter {:mvn/version "1.12.1"}}} Bare minimum code The lone -main function in src/first_principles/core.clj is the entry point of our web application. It contains a catch-all "handler" function that takes any incoming request and "echoes" it back as a string in the response. We compile and run this little web app as a Java process. (ns first-principles.core (:require [ring.adapter.jetty :as adapter]) (:gen-class)) (defn -main [& args] (adapter/run-jetty ; [1.] Jetty adapter's public API (fn [request] ; [2.] Handler function (required). {:status 200 :headers {"Content-Type" "text/plain;charset=UTF-8"} :body (str "echo request: " request)}) {:port 3000 ; [3.] Jetty server config. (optional) :join? false})) In the code above: 1. The Jetty adapter's public API expects: + A Ring-compliant handler function. + An optional configuration map. 2. Our bare minimum handler function. + The handler expects Ring-compliant request hash-maps, which the Ring Jetty adapter crafts for us. + And it must return Ring-compliant response hash-maps, for consumption by the adapter. 3. Optional Jetty server configuration. + Again, just a Clojure hash-map, also specified by the Ring spec. + Our Jetty adapter translates and applies any configuration we pass, to Jetty via its Java configuration API. Bare minimum live application Here is how we can run it from the terminal using Clojure CLI. Compile and run as a Java process. ^10 $ clj # in the root directory of our project Clojure 1.11.3 user=> (compile 'first-principles.core) first-principles.core # Ctrl-d to exit the REPL, then run the compiled code $ java --class-path $(clj -Spath) first_principles.core Or directly from the REPL session. $ clj # in the root directory of our project user=> (compile 'first-principles.core) first-principles.core user=> (first-principles.core/-main) ; start the server SLF4J: No SLF4J providers were found. SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details. #object[org.eclipse.jetty.server.Server 0x6331250e "Server@6331250e{STARTED}[11.0.20,sto=0]"] user=> ; Ignore the SLF4J messages. Ctrl-d to exit, when done. Bare minimum HTTP requests Our bare minimum live app responds to any HTTP request. Observe the request maps echoed back, for what changes, and what doesn't. * Try curl http://localhost:3000 , the bare minimum GET request. * Try other URI paths, with and without query params. * Try any of those combinations with other HTTP verbs e.g. curl -XPOST http://localhost:3000 (or -XDELETE or -XPUT or -XPATCH). Here is a sample result of a GET request to some made-up path with some arbitrary query parameters. $ curl -XGET \ "http://localhost:3000/foo/bar/baz?search=wassup%20world" echo request: {:ssl-client-cert nil, :protocol "HTTP/1.1", :remote-addr "127.0.0.1", :headers {"accept" "*/*", "user-agent" "curl/7.81.0", "host" "localhost:3000"}, :server-port 3000, :content-length nil, :content-type nil, :character-encoding nil, :uri "/foo/bar/baz", :server-name "localhost", :query-string "search=wassup%20world", :body #object[org.eclipse.jetty.server.HttpInput 0x2a91914a "HttpInput@714182986 cs=HttpChannelState@2eae00c0{s=HANDLING rs=BLOCKING os=OPEN is=IDLE awp=false se=false i=true al=0} cp=org.eclipse.jetty.server.BlockingContentProducer@6bac9b71 eof=false"], :scheme :http, :request-method :get} Though small, our "barebones" app is still doing a lot of stuff. To figure out what's going on, let's deconstruct it further. Bare minimum Ring project derived from first principles Hint: It's functions all the way down. Continuing with reference to the Big Picture, I feel like a minimal web application stack must, at the very least, facilitate the following: * Interface with the outside world, relative to our application. * Interface with us, in the language / domain of said app. * Provide creature comforts to automate the drudgery of interpreting HTTP requests and creating HTTP responses. * Provide some mechanism to orchestrate and control handler execution. It turns out that the mechanism of handlers alone is not enough to cater to all our request/response needs. We use another mechanism called "middleware". Interface with the outside world ring-jetty-adapter is our interface (ref: Big Picture). It is a Clojure wrapper over Jetty's Java APIs. For us "outside" is the land of Java objects, viz. Jetty's HTTP object model, Servlet interface, and server configuration interface. These bits of the library's "outside-facing" code illustrate how it "adapts" between Jetty and Clojure: * A Ring request can have a lot of stuff in it. For example, here is the function that moves HTTP request information from the Jetty server's request object into the corresponding Clojure hash-map that conforms to the Ring specification. Compare this with the response of the echo handler we saw a few paragraphs earlier. (defn build-request-map "Create the request map from the HttpServletRequest object." [^HttpServletRequest request] {:server-port (.getServerPort request) :server-name (.getServerName request) :remote-addr (.getRemoteAddr request) :uri (.getRequestURI request) :query-string (.getQueryString request) :scheme (keyword (.getScheme request)) :request-method (keyword (.toLowerCase (.getMethod request) Locale/ENGLISH)) :protocol (.getProtocol request) :headers (get-headers request) :content-type (.getContentType request) :content-length (get-content-length request) :character-encoding (.getCharacterEncoding request) :ssl-client-cert (get-client-cert request) :body (.getInputStream request)}) * A Ring response contains the HTTP status code, headers, and optional body. The adapter uses these functions to move information from a response hash-map, into the corresponding Jetty servlet response object. (defn update-servlet-response "Update the HttpServletResponse using a response map. Takes an optional AsyncContext." ([response response-map] (update-servlet-response response nil response-map)) ([^HttpServletResponse response context response-map] (let [{:keys [status headers body]} response-map] (when (nil? response) (throw (NullPointerException. "HttpServletResponse is nil"))) (when (nil? response-map) (throw (NullPointerException. "Response map is nil"))) (when status (.setStatus response status)) (set-headers response headers) (let [output-stream (make-output-stream response context)] (protocols/write-body-to-stream body response-map output-stream))))) Thankfully, weavejester has done all the heavy lifting for us, so we only have to care about the public API of this adapter library, which is a single function, run-jetty. Take a gander at its API doc. Interface with us ring-jetty-adapter is, again, our interface (ref: Big Picture). For Clojure programmers, generic Clojure data is our programming model, not custom objects. So the adapter's Clojure facing side lets us: * Configure Jetty from our Clojure app, using plain Clojure hash-maps. * Manipulate HTTP requests and responses from our Clojure app as plain Clojure hash-maps. * Rely on a standard specification of requests and responses as hash-maps that mirror the HTTP standard. The Clojure data version of Ring's request/response specification is human and machine readable (within our Clojure runtime). * A Ring Request map has a lot of stuff in it, as seen in the build-request-map function featured above. * The Ring response map is much simpler. A valid response is just the following hash-map. {:status 200 ; [1.] :headers {"Content-Type" "text/html;charset=UTF-8"} ; [2.] :body "