Examples

File uploads & static files

Trace multipart fields into nested maps and serve a local static directory.

Build and run

Requires Java 21+, Clojure CLI and the runtime named below. The example pins the compiler that supports native ^:async and await. Run these commands in a separate terminal from other examples.

git clone https://github.com/OUTCASTGEEK-TECH/fast-twitch.git
cd fast-twitch
git checkout a4eb73f96836d034c7a24fe09dc7d51e4c581f64
cd examples/04-file-upload-static-integrant-bulma
npm install
bb run-node

Send an upload and read a file

curl -i http://127.0.0.1:6464/static/readme.txt
curl -i -F 'upload[note]=from curl'   -F 'upload[file]=@static/readme.txt;type=text/plain'   http://127.0.0.1:6464/upload

The static request returns the text file. The upload response renders an HTML summary containing its filename, content type, byte size and note. Open / or /upload for the browser form. The example reports metadata; it does not save uploads to disk.

Understand the multipart map

wrap-multipart-params reads native FormData. A file field becomes a map containing :filename, :content-type, :size, :file and :tempfile. The last two refer to the native file object; :tempfile is not a filesystem pathname. Repeated fields form vectors.

Nested parsing turns upload[file] into a field under "upload"; keyword conversion allows (:file (:upload params)). summarize-upload reads this map and builds a smaller value for the view. The view does not consume the file bytes.

Trace the wrappers

wrap-file is outermost: it can answer /static/... without invoking upload routes. Otherwise the request continues through multipart parsing, nested parsing, keyword conversion, then routing. The source below shows every wrapper in order and the static-root Integrant dependency.

Read file bytes when needed

Use the native :file object's asynchronous .arrayBuffer or .text method in an async handler when you actually need contents. Validate the size and media type for your application's rules before using those contents. The client-supplied MIME type alone does not establish the file's format. A filesystem write is a separate application operation.

Exercises and failure cases

Request /missing and inspect status 404. Submit a note without choosing a file and inspect the missing-file summary. A URL containing a path traversal attempt must not expose files outside the configured static root. Avoid concurrently running another example on port 6464; use PORT=7777 bb run-node if required.

Static-file guide · Multipart API.

Complete source and build files

The files below are copied without abbreviation from Fast-Twitch revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64. The walkthrough and API reference use the same revision.

Source: examples/04-file-upload-static-integrant-bulma/src/uploadapp/server.cljs

View this exact file on GitHub.

(ns uploadapp.server
  [:require-macros [fast-twitch.macros :refer [env-var]]]
  [:require [fast-twitch.middlewares.file :as file]
            [fast-twitch.middlewares.keyword-params :as keyword-params]
            [fast-twitch.middlewares.multipart-params :as multipart-params]
            [fast-twitch.middlewares.nested-params :as nested-params]
            [fast-twitch.routing :as routing :refer [start-server! stop-server!]]
            [integrant.core :as ig]
            [replicant.string :as html]
            [uploadapp.views :as views]]
  [:refer-global :only
                 [Number console globalThis]])

(def request-options
  {:protocol "HTTP/1.1" :remote-addr "127.0.0.1"})

(defn html-response
  ([page]
   (html-response 200 page))
  ([status page]
   {:status status
    :headers {"Content-Type" "text/html; charset=utf-8"}
    :body (html/render page)}))

(defn missing-file-summary
  []
  {:filename "No file selected" :content-type "n/a" :size 0 :note "n/a"})

(defn summarize-upload
  [params]
  (let [upload (:upload params)
        file (:file upload)]
    (if file
      {:filename (or (:filename file) "unnamed")
       :content-type (or (:content-type file) "not provided")
       :size (or (:size file) 0)
       :note (or (:note upload) "")}
      (missing-file-summary))))

(defn not-found-page
  [settings uri]
  (html-response 404 (views/not-found-page settings uri)))

(defmethod ig/init-key ::settings
  [_ options]
  options)

(defmethod ig/init-key ::pages
  [_ {:keys [settings]}]
  {:home (fn [_request]
           (html-response (views/home-page settings)))
   :upload (fn [{:keys [params]}]
             (let [summary (summarize-upload params)]
               (console/log (str "Upload summary: " (pr-str summary)))
               (html-response (views/uploaded-page settings summary))))
   :not-found (fn [{:keys [uri]}]
                (not-found-page settings uri))})

(defmethod ig/init-key ::routes
  [_ {:keys [pages]}]
  [{:pattern "/" :method :get :handler (:home pages)}
   {:pattern "/upload" :method :get :handler (:home pages)}
   {:pattern "/upload" :method :post :handler (:upload pages)}])

(defmethod ig/init-key ::app
  [_ {:keys [routes pages static-root]}]
  (-> (routing/routes routes (:not-found pages))
      (keyword-params/wrap-keyword-params)
      (nested-params/wrap-nested-params)
      (multipart-params/wrap-multipart-params)
      (file/wrap-file static-root {:url-root "static"})))

(def config
  {::settings {:title "File Upload and Static Files"
               :bulma-css "https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css"}
   ::pages {:settings (ig/ref ::settings)}
   ::routes {:pages (ig/ref ::pages)}
   ::app {:routes (ig/ref ::routes) :pages (ig/ref ::pages) :static-root "static"}})

(defonce system
  (ig/init config))

(def handler
  (routing/ft-handler (::app system) request-options))

(defn shutdown
  []
  (stop-server! :force true
                :callback (fn []
                            (console.log "Server stopped"))))

(defn run
  []
  (start-server! handler
                 {:port (Number (or (env-var "PORT") 6464))
                  :hostname (or (env-var "HOSTNAME") "127.0.0.1")
                  :onListen (fn [addr]
                              (console/log (str
                                             "Upload/static server listening at http://"
                                               (aget addr "hostname")
                                             ":" (aget addr "port"))))}))

(set! *main-cli-fn* run)

(defn ^:export fetch
  [request]
  (handler request))

Source: examples/04-file-upload-static-integrant-bulma/src/uploadapp/views.cljs

View this exact file on GitHub.

(ns uploadapp.views)

(defn layout
  [{:keys [title bulma-css]} & body]
  [:html
   [:head [:meta {:charset "utf-8"}]
    [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
    [:title title] [:link {:rel "stylesheet" :href bulma-css}]]
   [:body
    [:section.section
     [:div.container [:h1.title title]
      [:p.subtitle
       "Upload a file, inspect the request map data, and fetch a static file."] body]]]])

(defn upload-form
  []
  [:form.box {:method "post" :action "/upload" :enctype "multipart/form-data"}
   [:div.field [:label.label {:for "upload-note"} "Note"]
    [:div.control
     [:input.input
      {:id "upload-note"
       :type "text"
       :name "upload[note]"
       :placeholder "Why are you uploading this file?"}]]]
   [:div.field [:label.label {:for "upload-file"} "File"]
    [:div.control [:input.input {:id "upload-file" :type "file" :name "upload[file]"}]]]
   [:div.field.is-grouped
    [:div.control [:button.button.is-link {:type "submit"} "Upload"]]
    [:div.control [:a.button.is-light {:href "/static/readme.txt"} "Open static file"]]]])

(defn home-page
  [settings]
  (layout settings
          [:div.columns [:section.column.is-two-thirds (upload-form)]
           [:aside.column
            [:div.notification.is-info.is-light [:p "The form field names are nested:"]
             [:pre "upload[note]\nupload[file]"]
             [:p "After middleware runs, the handler can read:"]
             [:pre "[:params :upload :file]"]]]]))

(defn upload-summary
  [summary]
  [:div.box [:h2.title.is-4 "Upload summary"]
   [:table.table.is-fullwidth
    [:tbody [:tr [:th "File name"] [:td (:filename summary)]]
     [:tr [:th "Browser content type"] [:td (:content-type summary)]]
     [:tr [:th "Size"] [:td (str (:size summary) " bytes")]]
     [:tr [:th "Note"] [:td (:note summary)]]]]
   [:p [:a.button.is-link {:href "/"} "Upload another file"]]])

(defn uploaded-page
  [settings summary]
  (layout settings (upload-summary summary)))

(defn not-found-page
  [settings uri]
  (layout settings
          [:div.notification.is-warning [:strong "No route matched "] [:code uri]]))

Source: examples/04-file-upload-static-integrant-bulma/deps.edn

View this exact file on GitHub.

{:aliases {:nREPL {:extra-deps {cider/cider-nrepl {:mvn/version "0.62.2"}
                                cider/piggieback {:mvn/version "0.7.0"}
                                nrepl/nrepl {:mvn/version "1.7.0"}}
                   :main-opts
                     ["-m" "nrepl.cmdline" "--middleware"
                      "[cider.nrepl/cider-middleware, cider.piggieback/wrap-cljs-repl]"]}
           :test {:extra-paths ["test"]}}
 :deps {fast-twitch/fast-twitch {:local/root "../.."}
        integrant/integrant {:mvn/version "1.0.1"}
        no.cjohansen/replicant {:mvn/version "2026.07.1"}
        org.clojure/clojurescript {:git/sha "56a94ac13bfac22108e4d868dfd4763edaceb864"
                                   :git/url "https://github.com/clojure/clojurescript"}}
 :paths ["src"]}

;; Start the example nREPL server
;; clj -M:nREPL

Source: examples/04-file-upload-static-integrant-bulma/app.edn

View this exact file on GitHub.

{:aot-cache true
 :cache-analysis true
 :dump-core true
 :fn-invoke-direct true
 :infer-externs :all
 :language-in :es-next
 :language-out :es-next
 :main uploadapp.server
 :optimizations :simple
 :optimize-constants true
 :output-dir "build"
 :output-to "server.cjs"
 :output-wrapper "%s\nexports.fetch = uploadapp.server.fetch;\n"
 :parallel-build true
 :pretty-print true
 :source-map "server.cjs.map"
 :stable-names true
 :static-fns true
 :target :nodejs}

Source: examples/04-file-upload-static-integrant-bulma/bb.edn

View this exact file on GitHub.

{:tasks {build {:doc "Compile ClojureScript to server.cjs"
                :task (shell "clj" "-M" "-m" "cljs.main" "-co" "app.edn" "-c")}
         deno-serve {:doc "Build and serve the exported Fetch handler with Deno"
                     :task (let [port (or (System/getenv "PORT") "6464")
                                 jobs (or (System/getenv "DENO_JOBS") "1")]
                             (shell "clj"
                                    "-M"
                                    "-m"
                                    "cljs.main"
                                    "-co"
                                    "app.edn"
                                    "-co"
                                    (pr-str {:nodejs-rt false
                                             :output-dir "build/serve"
                                             :output-to "build/server-serve.cjs"
                                             :source-map "build/server-serve.cjs.map"})
                                    "-c")
                             (shell {:extra-env {"DENO_JOBS" jobs "PORT" port}}
                                    "deno"
                                    "serve"
                                    "--parallel"
                                    "-A"
                                    "--port"
                                    port
                                    "build/server-serve.cjs"))}
         run-bun {:depends [build]
                  :doc "Build and run server.cjs with Bun"
                  :task (shell "bun server.cjs")}
         run-deno {:depends [build]
                   :doc "Build and run server.cjs with Deno"
                   :task (shell "deno run -A server.cjs")}
         run-node {:depends [build]
                   :doc "Build and run server.cjs with Node"
                   :task (shell "node server.cjs")}}}

Source: examples/04-file-upload-static-integrant-bulma/package.json

View this exact file on GitHub.

{
  "name": "fast-twitch-04-file-upload-static-integrant-bulma",
  "private": true,
  "type": "commonjs",
  "scripts": {
    "build": "bb build",
    "node": "bb run-node",
    "deno": "bb run-deno",
    "bun": "bb run-bun"
  },
  "devDependencies": {
    "@cljs-oss/module-deps": "^1.1.1"
  }
}

Source: examples/04-file-upload-static-integrant-bulma/deno.jsonc

View this exact file on GitHub.

{
  "tasks": {
    "build": "bb build",
    "start": "bb run-deno",
    "serve": "bb deno-serve"
  }
}

Source: examples/04-file-upload-static-integrant-bulma/static/readme.txt

View this exact file on GitHub.

This file lives in examples/04-file-upload-static-integrant-bulma/static.

fast-twitch is serving it through wrap-file at /static/readme.txt.

Source revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64

Built with Fast-Twitch · Integrant · Datastar

Search documentation

Search pages and API symbols

Start hereWelcome to Fast-TwitchRing HTTP applications and configured native clients and transports in ClojureScript.Start hereGetting startedCompile and run your first Fast-Twitch server.Core conceptsRequest & responseUnderstand the maps at the center of a Fast-Twitch application.Core conceptsRoutingMatch URL patterns and compose handlers.Core conceptsMiddlewareWrap handlers with focused request and response behavior.GuidesHTML & IntegrantCompose a UI system with Integrant and render Hiccup.