JanetDocsSourcePlaygroundTutorialsI'm Feeling luckyCommunityGitHub sign in

Joy is a powerful web framework, made by Swlkr. The docs are here, but here I will try to explain how it works from simpler to more complex sites to empower users.

N.b. Every example requires: (import joy :as j)

Minimal Working Site
(defn home [request]
    (j/text/html
        [:h1 "hi"]
        [:p "more text"]))

(j/server (j/app {:routes (routes [:get "/" :home])}) 9001)

Spreading it out:

(defn home [request]
  (j/text/html [:body
   [:p "hi"]]))

(def routes
  [[:get "/" home]])

(def app (j/handler routes))

(defn main [& args]
  (j/server app 9001))

Linking pages with joy/url-for

(defn home [request]
  (j/text/html
   [:body
    [:p "You can read "]
    [:a {:href (j/url-for :about)} "about us"]]))
(defn about [request]
  (j/text/html
   [:body
    [:p "We are people. Return "]
    [:a {:href (j/url-for :home)} "home"]]))

(j/defroutes routes
  [:get "/" home]
  [:get "/about" about])
(def app (j/handler routes))

(defn main [& args]
  (j/server app 9001))

layouts

(defn layout [{:body body :request request}]
  (j/text/html
   (j/doctype :html5)
   [:html {:lang "en"}
    [:head
     [:title "This site"]]
    [:body body]]))
    
(defn hi [r]
#   (j/text/html [:body [:p "I'm here"]]) # replaced this with a function
 (layout {:request r :body [:p "I'm here"]}))

``
Joy will route things through the `layout` function if your function just returns an array `[ ...]` and you use:

``

Working Small Site

(import joy :as j)

(defn layout [{:body body :request request}]
  (j/text/html
   (j/doctype :html5)
   [:html {:lang "en"}
    [:head
     [:title "This site"]]
    [:body body]]))

(defn home [request]
  [:p "You can read " [:a {:href (j/url-for :about)} "about us"]])
(defn about [request]
  [:p "We are people. Return " [:a {:href (j/url-for :home)} "home"]])
(defn /404 [request]
  (as-> (layout {:request request
                 :body [:center
                        [:h1 "Oops! 404!"]]}) ?
        (put ? :status 404)))

(j/defroutes routes
  [:get "/" home]
  [:get "/about" about])

(def app
  (j/app {:routes routes
          :404 /404
          :layout layout}))

(defn main [& args]
  (j/server app 9001))

Requests and Responses are all dictionaries

Our functions handling specific routes all take a request and turn it into a response. They are all dictionaries. An example request:

{:uri "/"
 :method "GET"
 :headers {"Accept" "text/html"}}

Middleware or more complex handlers can add fields and do useful stuff with this.

joy/app abstracts away middleware

(def app (j/app {:routes routes
   :404 /404
   :layout layout}))

is the same as:

(def app (-> (j/handler routes)
             (j/layout layout)
             (j/not-found /404)
             (j/logger)))

Joy passes a request through many functions to generate a webpage. joy/app abstracts many of them away with a hashmap. While you can pass it whatever you want, it has these defaults:

{:routes (auto-routes)
 :layout false
 :extra-methods true
 :query-string true
 :body-parser true
 :json-response false
 :json-body-parser true
 :logger {}
 :csrf-token true
 :session {}
 :x-headers {}
 :server-error true
 :404 true
 :static-files true}

Which map to these middleware:

[(handler)
 (layout layout-handler) # Use declared layout if handler returns an [array]
 (with-csrf-token) # Generate unique token then check incoming requests for it, for security
 (with-session) # Read cookie and add session ID at (request :session)
 (extra-methods) # HTML forms only support GET and POST, this hacks more in
 (query-string) # Add (request :query-string) if route has "?" like: /search?q=janet
 (body-parser) # Turn form input into dictionary at (request :body)
 (json-body-parser) # Turn JSON string into dictionary at (request :body)
 (json-response) # Return JSON if handler returns a {:table "dictionary"}
 (server-error) # Cache stack trace when server crashes, different in prod and dev
 (x-headers) # Add security headers like X-Frame-Options
 (static-files) # Serve a file directly, ignoring middleware. 
 (not-found) # Serve a "not found" page
 (cors) # Let other websites access this server "Cross Origin Resource Sharing"
 (file-uploads) # Hold file in temp/location and return this body @[{:filename "name of file" :content-type "content-type" :size 123 :tempfile "<file descriptor>"}]
 (logger)] # Print log of served pages to the terminal running the server

These and more middleware are implemented here.

This functional middleware architecture is similar to Clojure's Ring or Ruby's Rack. It works like a pipeline or assembly line where a request goes through many filters until reaching the handler, then travels back.

The joy/handler function searches a *routes* array and if it finds a match, bundles the parameters into the request and calls the function you've defined. It does this via route? which compares the request method (GET, POST etc.) and URL with defined routes. Web frameworks exist to ease things like routing, so Joy provides many features.

Using a Database

First we need a database. In the terminal, type sqlite3 test.sqlite then enter CREATE TABLE account (id integer primary key, name text);, finally INSERT INTO account VALUES (1, "bob");. This made an example database file. (j/db/connect "test.sqlite") opens it from Janet.

A minimal example:

(import joy :as j)

(defn data-query [request]
  (j/text/html [:body [:p (((j/db/from :account) 0) :name)]]))
# (j/db/from :account) returns every row in table account, 0 shows the first row and :name gets the name

(j/defroutes routes
  [:get "/db" data-query])

(def app (-> (j/handler routes)
             (j/logger)))

(defn main [&amp; args]
  (j/db/connect "test.sqlite")
  (j/server app 9001)
  (j/db/disconnect))

(db/connect) alone will read from a file named .env e.g.:

DATABASE_URL=test.sqlite
ENCRYPTION_KEY=sdlfsjfslfjslafjkl
JOY_ENV=development
PORT=9001

Database Queries

Joy has my favorite DSL for querying databases, explained here. I will show you this magic, but for now, here are some example queries from this very website:

(db/find-by :account :where {:login login})

(db/insert :example {:account-id (account :id)
                     :account-login (account :login)
                     :binding-id (binding :id)
                     :binding-name (binding :name)
                     :package-id (package :id)
                     :package-name (package :name)
                     :body (body :body)})

(defn find-or-insert [tbl query]
  (if-let [q (db/find-by tbl :where query)]
    q
    (db/insert tbl query)))

(joy/db/insert :package {:name name 
                         :url url})

(joy/db/update :binding b {:docstring (d :docstring)
                           :name (d :name)})

(joy/db/find-by :link :where {:source (string source)
                              :target (string target)})