This guide describes the architecture of Sweet Tooth API servers and the tools used to implement the architecture. It covers:
The term component is super generic! It’s siblings with module and sub-system in the "nebulous programming terms" family. For our purposes a component is "an instance of a computing thing that complies with an interface." An interface is a set of rules for sending and receiving messages, including both the transmission mechanism (direct function call, network call, etc) and the message structure.
For example, a machine is a computing thing with a TCP/IP interface. An HTTP server is a computing thing with an HTTP interface. Let’s look at these components more closely.
The "and Beyond" part of this guide’s title is bs, but it’s lovely bs. There’s a retail chain in America called Bed Bath and Beyond and the "and beyond" totally kills me. For some reason it sounds like they could make your wildest dreams come true, but no, the "beyond" part is actually useless Americana tchotchkes like Mickey Mouse mini waffle makers. Related: I once found a blow dryer at Target whose packaging read something like: "Good for drying hair AND SO MUCH MORE" and I sincerely hope the copywriter enjoyed writing that. Thank you, noble soul, for reminding me that all of life is full of wondrous possibility. Even everyday consumer goods hold untold potential. Sweet Tooth: good for writing single page apps AND SO MUCH MORE |
Handling an HTTP request requires the coordination of multiple components. This section breaks down high-level components into their subcomponents and describes their relationships.
Check out my this diagram of the components involved in handling an API request which looks like it was outsourced to a three year old:
The highest-level component is the machine (1). When an HTTP request is sent, the requestor’s only expectation is that some machine - virtual private server (VPS), Amazon EC2 instance, a lemon-powered raspberry pi - somewhere receives the request and sends a response. It doesn’t care about the machine’s OS or what processes are running.
The machine will be running a database (2). It will also be running an HTTP server (3). There’s nothing special about HTTP servers; they’re just some program someone wrote, only they happen to listen for HTTP requests and send HTTP responses.
For Clojure web apps, the HTTP server usually creates a database connection (4). The HTTP server also contains a request handler (5). In Clojure apps, the request handler is just a function which takes a request and returns a response.
the next couple paragraphs are a light overview of how a request gets transformed at every stage of its journey from machine to HTTP server to Clojure request handler. I’d love feedback on it: does it not provide enough detail? Does it belong elsewhere? |
The relationship between machine, HTTP server, and handler, is represented by the boxes and lines at (6). The boxes represent an interface that communicate with the external world via some protocol. A protocol in this context is just some agreed-upon message structure that allows sender and receiver to understand each other. The machine interface is an open port, typically port 80 or port 443, and communication happens via TCP/IP.
The HTTP server is some process running on the machine assigned to listen to ports 80 and 443. It receives and sends messages that conform to the HTTP protocol. Internally, the HTTP server converts the raw text of an HTTP message into data structures that its supported programming languages understand. An Apache server has modules for converting a message to PHP or Perl. A Ruby server converts the message to some Ruby object.
Clojure is hosted on the JVM, and we use Java HTTP servers, most frequently a
library called Jetty. Jetty converts a request to a Java object. In Clojure, we
prefer to work with native Clojure maps and vectors, and developers most
frequently use the ring library to adapt the request for Clojure. It converts
the Java request object into a Clojure map with the keys :uri
,
:query-string
, :request-method
, :headers
, and :body
, plus a few more.
Ring allows you to define a Clojure function to handle requests - we saw that at
(5). The request function takes a Ring request as an argument and should return
a response, which is a map with the keys :status
, :headers
, and :body
.
You can write a function to construct a Ring response any way you want to, but
generally Ring request handlers are structured as a middleware stack (7) and a
router (8). The Ring request map passes through the middleware stack, which
transforms the request by adding, modifying, or removing the map’s keys. The
ring request is then passed to the router, which routes the request, passing it
on to an endpoint handler based on the request’s path (e.g. /topic/1
) and
method (:get
, :post
, etc).
Endpoint handlers typically perform CRUD (create, read, update, delete) operations on a database, and therefore they typically have a reference to the database connection (9).
Now that we know what components are involved in building an API server and how those components are related to each others, let’s turn our attention toward the work we as developers have to do to implement this architecture. Implementing an architecture includes addressing how you define, compose, and initialize a system’s components.
To define a component is to establish its responsibilities and its interface. It also means choosing one or more language constructs to implement the notion of "component".
In object-oriented languages this process feels more solid somehow: components are defined by classes; the class’s public methods are the interface and the notion of "component" maps directly to classes. Things feel a bit more loosey-goosey in Clojure land — is a component a function? a namespace? a record? — but I’ll introduce you to techniques for defining components shortly.
Composing components: how do components reference each other? The two main approaches are to create a globally-accessible component that other components reference directly from anywhere, or to follow the dependency injection pattern. You’ll soon learn about how Sweet Tooth relies on the Integrant and Duct libraries, which implement dependency injection for Clojure apps.
Initializing components refers to the process of creating any objects or state the component needs, and calling a function or method to start the component if necessary. To initialize a request handler, you just create a function. To initialize a database connection pool you create an instance of a connection pool service, which might create some initial threads for db connections.
To get a Clojure API server running, you must first get a JVM process running. Within that process, you must initialize components in dependency order:
Initialize a database connection or connection pool
Initialize a request handler that references the database connection
Initialize an HTTP server with the request handler
What does it mean to "initialize an HTTP server" from within a JVM process? If you’re familiar with programs like Apache or nginx, you might be used to thinking of an HTTP server as a program that you launch from the command line, not as something that you start from within the process of a program you’re writing. The thing is, anyone can just write a program that starts listening to a port. The tools are readily available. If you use your programming language’s standard libary to start listening for messages on a port and responding, congratulations: you’ve created a server! Now if you care about things like performance and resilience, you’ll have to get a bit fancier. That’s why we have HTTP server libriaries. In the Java world, one of the most popular libraries is Jetty. It adds some structure to how HTTP requests are handled, and it takes care of managing resources like threads. Initializing a Jetty server in your JVM process is basically a matter of
creating an |
You could easily write something like this pseudocode to define, compose, and initialize your system’s components:
(def db-connection (create-connection))
(defn handler [req] (update-db db-connection))
(defn start-server [] (run-jetty handler {:port 3000}))
(start-server)
I’ve seen plenty of Clojure API servers with code that looks like that, and that approach works fine.
As I’ve mentioned like a billion times now, Sweet Tooth uses Integrant and Duct to manage these architectural concerns. We’ll first look at Integrant, because it provides the foundation. Then we’ll look at Duct, a layer on top of Integrant that 1) makes it easier to create bundles of components to share and 2) makes it easy to configure components for different environments (dev, test, prod, etc).
So let’s look at Integrant so that you won’t have to listen to me say "In a minute we’re going to look at Integrant" anymore.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close