Building Web Services

The golisp/web package provides a Ring-style HTTP framework: handlers are plain functions, requests and responses are maps.

Handlers

A handler takes a request and returns a response:

(ns main (:import [golisp/web]))

(defn hello [req web/Request] -> web/Response
  (web/json-response 200 {"message" "hello"}))

web/Request and web/Response are map[string]any — inspect and build them with standard map operations.

Routing

defroutes defines a web/Handler from a list of routes. The verb macros (GET, POST, PUT, DELETE, PATCH) turn each body into a handler, the vector after the path binds named path parameters as locals, and req is in scope throughout. An optional :middleware clause wraps the whole chain (outermost first):

(defroutes app
  :middleware
  [web/wrap-logging web/wrap-recover web/wrap-cors web/wrap-json]
  (GET    "/"          [] (home req))
  (GET    "/users"     [] (web/json-response 200 (all-users)))
  (GET    "/users/:id" [id]
    (if-let [u (find-user id)]
      (web/json-response 200 u)
      (web/not-found "user not found")))
  (POST   "/users"     [] (create-user req))
  (DELETE "/users/:id" [id] (delete-user id)))

A raw (web/get path handler) still composes inside defroutes, which is handy for a route that needs its own per-route middleware:

(defroutes app
  (GET "/" [] (home req))
  (web/get "/export" (web/wrap export-handler web/wrap-auth)))

Under the hood the DSL expands to web/routes plus web/wrap. You can write that lower-level form directly when you don't want path-param binding:

(def app web/Handler
  (web/routes
    (web/get  "/"          home)
    (web/post "/users"     create-user)))

Request Helpers

(web/path-param  req "id")           ; URL parameter
(web/query-param req "page")         ; ?page=2
(web/header      req "content-type") ; request header
(web/body-map    req)                ; parsed JSON body as map

Response Helpers

(web/json-response 200 {:id 1 :name "Alice"})
(web/text-response 200 "plain text")
(web/html-response 200 "<h1>Hello</h1>")
(web/redirect "/login")
(web/no-content)
(web/bad-request    "missing field")
(web/unauthorized   "invalid token")
(web/not-found      "user not found")
(web/server-error   "internal failure")

Middleware

Wrap handlers with middleware using web/wrap. Middleware applies right-to-left:

(def app web/Handler
  (web/wrap
    (web/routes ...)
    web/wrap-logging    ; logs each request
    web/wrap-recover    ; catches panics
    web/wrap-cors       ; adds CORS headers
    web/wrap-json))     ; parses JSON bodies

Write your own middleware:

(defn wrap-auth [handler web/Handler] -> web/Handler
  (fn [req web/Request] -> web/Response
    (let [token (web/header req "authorization")]
      (if (valid-token? token)
        (handler (assoc req "identity" (parse-token token)))
        (web/unauthorized "invalid token")))))

HTML with Hiccup

Build HTML from nested vectors:

(web/html
  [:html
    [:head [:title "My App"]]
    [:body
      [:h1 {:class "title"} "Hello"]
      [:ul
        (map (fn [item] [:li item]) items)]]])

Starting the Server

(defn main [] -> void
  (let [port (sys/env "PORT" "8080")]
    (println "listening on :" port)
    (web/serve-graceful (str ":" port) app)))

web/serve-graceful handles OS signals (SIGTERM, SIGINT) and drains in-flight requests before shutting down.

Complete Example: Task API

(ns main (:import [golisp/web]))

(def tasks []any
  [{"id" "1" "title" "Buy groceries" "done" false}
   {"id" "2" "title" "Write code"    "done" true}])

(defn create-task [req web/Request] -> web/Response
  (if-let [body (web/body-map req)]
    (let [{title :title :- string} body]
      (if (= title "")
        (web/bad-request "title is required")
        (web/json-response 201 {"id" "new" "title" title "done" false})))
    (web/bad-request "invalid JSON body")))

(defroutes app
  :middleware
  [web/wrap-logging web/wrap-recover web/wrap-json]
  (GET "/tasks" []
    (switch (web/query-param req "done")
      "true"  (web/json-response 200 (filter (fn [t] (get t "done")) tasks))
      "false" (web/json-response 200 (filter (fn [t] (not (get t "done"))) tasks))
      :default (web/json-response 200 tasks)))
  (GET "/tasks/:id" [id]
    (if-let [task (some (fn [t] (if (= (get t "id") id) t nil)) tasks)]
      (web/json-response 200 task)
      (web/not-found (str "task " id " not found"))))
  (POST "/tasks" [] (create-task req)))

(defn main [] -> void
  (println "Tasks API on :4000")
  (web/serve-graceful ":4000" app))

Server-Sent Events and WebSocket

; SSE — pass a channel; the framework streams values as events
(defn events [req web/Request] -> web/Response
  (let [ch (chan string 10)]
    (go
      (doseq [i (range 5)]
        (send! ch (str "event " i))
        (time/sleep (* 500 time/Millisecond)))
      (close! ch))
    (web/sse-response ch)))

; WebSocket — handler receives in/out channels
(defn chat [req web/Request] -> web/Response
  (web/websocket
    (fn [req web/Request in (chan string) out (chan string)] -> void
      (for-chan [msg in]
        (send! out (str "echo: " msg))))))