{"id":26194044,"url":"https://github.com/clojurestream/kit-workshop","last_synced_at":"2025-04-15T03:14:58.500Z","repository":{"id":199801187,"uuid":"702672801","full_name":"clojurestream/kit-workshop","owner":"clojurestream","description":"Files for Kit Workshop at ClojureStream","archived":false,"fork":false,"pushed_at":"2023-10-15T22:30:59.000Z","size":98,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-15T03:14:53.030Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clojurestream.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-09T19:13:20.000Z","updated_at":"2025-02-25T16:53:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"f3a577b9-a8b1-4a1c-a5c8-e051b6cdf189","html_url":"https://github.com/clojurestream/kit-workshop","commit_stats":null,"previous_names":["clojurestream/kit-workshop"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clojurestream%2Fkit-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clojurestream%2Fkit-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clojurestream%2Fkit-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clojurestream%2Fkit-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clojurestream","download_url":"https://codeload.github.com/clojurestream/kit-workshop/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248997079,"owners_count":21195799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-12T01:54:36.891Z","updated_at":"2025-04-15T03:14:58.484Z","avatar_url":"https://github.com/clojurestream.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kit Workshop\n\n## Table of Contents\n\n- [Kit Workshop](#kit-workshop)\n  - [Table of Contents](#table-of-contents)\n  - [Prerequisites](#prerequisites)\n    - [Setup](#setup)\n    - [Intro to Clojure](#intro-to-clojure)\n    - [Conduct During the Workshop](#conduct-during-the-workshop)\n  - [Kit Workshop](#kit-workshop-1)\n    - [SECTION 1](#section-1)\n    - [Creating a Project](#creating-a-project)\n    - [kit.edn](#kitedn)\n    - [Starting the REPL](#starting-the-repl)\n    - [Using Modules](#using-modules)\n    - [SECTION 2](#section-2)\n    - [What are Modules](#what-are-modules)\n    - [Adding a Database](#adding-a-database)\n    - [SECTION 3](#section-3)\n    - [Managing the Database](#managing-the-database)\n    - [Querying the Database](#querying-the-database)\n    - [SECTION 4](#section-4)\n    - [Routing](#routing)\n    - [SECTION 5](#section-5)\n    - [Adding Dependencies](#adding-dependencies)\n    - [Creating Integrant Components](#creating-integrant-components)\n      - [Integrant Component Lifecycle](#integrant-component-lifecycle)\n      - [Compoent Lifecycle Multimethods](#compoent-lifecycle-multimethods)\n      - [Loading Integrant Components](#loading-integrant-components)\n      - [Trying Things Out in the REPL](#trying-things-out-in-the-repl)\n      - [Wiring Up Our New Component](#wiring-up-our-new-component)\n    - [Processing GIF Payload](#processing-gif-payload)\n      - [Error Handling](#error-handling)\n      - [Swagger API Testing](#swagger-api-testing)\n    - [SECTION 6](#section-6)\n    - [Testing with the REPL](#testing-with-the-repl)\n    - [SECTION 7](#section-7)\n\n## Prerequisites\n\nmacOS or Linux recommended.\n\n### Setup\n\nMake sure you have the following dependencies installed:\n\n- Clojure CLI https://clojure.org/guides/install_clojure\n- Java 11+ (17 recommended)\n- Docker and [docker compose](https://docs.docker.com/compose/gettingstarted/). Check that you can run `docker compose up` on a simple `docker-compose.yml` file. (alternatively make sure you have a locally running Postgres on your machine)\n- VSCode with Calva, or editor of preference as long as it's nREPL compatible (Cursive, Emacs, etc.)\n\nOptionally, we recommend installing Babashka to run the frontend script that will be used to test the project\n- Babashka https://babashka.org\n\nClone this repository and make sure you can run the project by doing the following\n\n```bash\ngit clone git@github.com:clojurestream/kit-workshop.git\ncd kit-workshop\nclj -M:dev:nrepl\n```\n\nThis should start up a REPL prompt, it might output something like:\n\n```\nnREPL server started on port 50354 on host localhost - nrepl://localhost:50354\nnREPL 0.9.0\nClojure 1.11.1\nOpenJDK 64-Bit Server VM 17.0.1+12-LTS\nInterrupt: Control+C\nExit:      Control+D or (exit) or (quit)\n```\n\nInside the prompt, test that you can start the application server by running:\n\n```clojure\n(go)\n```\n\nThen go to [http://localhost:3000/api/health](http://localhost:3000/api/health) to check that the server is running.\n\n### Intro to Clojure\n\nThis workshop assumes a basic familiarity with Clojure and functional programming. If you are new to Clojure, we can recommend the following resources to get started:\n\n- High level overview https://yogthos.github.io/ClojureDistilled.html\n- An Animated Introduction to Clojure https://markm208.github.io/cljbook/\n- Clojure from the Ground Up https://aphyr.com/tags/Clojure-from-the-ground-up\n- Clojure for the Brave and True https://www.braveclojure.com/foreword/\n\nOf course you are also welcome to search around and find other resources suited to your learning style or interests.\n\nWe will **not** be going over the basics of Clojure during the workshop.\n\n### SECTION 1\n\n### Conduct During the Workshop\n\nSome general guidelines during the workshop\n\n- By attentding this workshop, you agree to the [Code of Conduct](/CONDUCT.md)\n- If you get stuck, questions are encouraged\n- There is a checkpoint branch at the end of each section to make sure everyone's on the same page.\n\n## Kit Workshop\n\n### Creating a Project\n\nKit uses [clj-new](https://github.com/seancorfield/clj-new) to create projects from the template. If you don't already have it installed on your local machine, you can pull it in by running\n\n```bash\nclojure -Ttools install com.github.seancorfield/clj-new '{:git/tag \"v1.2.381\"}' :as new\n```\n\nNow we can create our new project by running.\n\n```bash\n clojure -Tnew create :template io.github.kit-clj :name io.github.kit/gif2html\ncd gif2html\n```\n\nLet's initialize git so that we can reference the checkpoints in the workshop in case we run into trouble along the way.\n\n```bash\ngit init\ngit checkout -b workshop\ngit add .\ngit commit -a -m \"initial commit\"\ngit remote add origin git@github.com:clojurestream/kit-workshop.git\ngit fetch \n```\n\nYou should now have a project with the following folders\n\n```\n├── env\n│   ├── dev/clj/io/github/kit/gif2html\n│   │   └── resources\n│   ├── prod/clj/io/github/kit/gif2html\n│   │   └── resources\n│   └── test\n│       └── resources\n├── resources\n├── src/clj/io/github/kit/gif2html/web\n│                                  ├── controllers\n│                                  ├── middleware\n│                                  └── routes\n└── test/clj/io/github/kit/gif2html    \n```\n\nLet's take a look at what these folders are and their purpose.\n\n* `env` - This folder contains environment dependent code.\n  * `dev` - The code in this folder will only be run during development.\n  * `prod` - The code in this folder will be compiled into the uberjar when the application is packaged for deployment. \n* `resources` - This folder contains static assets such as configuration files, HTML templates, and so on.\n* `src/clj` - This folder contains the application code.\n  * `controllers` - This package contains namespaces that handle your application business logic.\n  * `middleware` - This package contains Ring routing middleware that encapsulates cross-cutting logic shared across the routes.\n  * `routes` - This package is where server endpoints are defined.\n* `test` - This folder contains the tests.\n\n### deps.edn\n\nThis file contains the dependencies and aliases used to manage our project. We can see that the file has a few libraries listed under dependencies that provide routing, logging, and other useful functionality for our project.\n\n### kit.edn\n\nKit uses a module system that allows adding new functionality to existing Kit projects by installing modules from the REPL.\nThis file contains metadata about the project and referenes to module repositories that will be used to add new modules in the project.\n\nKit modules are templates that get injected in the project and generate code within exisitng project files. The metadata in `kit.edn` is\nused to specify the paths and namespaces for the generated code.\n\n### Starting the REPL\n\nThe REPL can be started by running the following command from the project folder:\n\n```shell\nclj -M:dev:nrepl\n```\n\nOnce the REPL starts you should see the following in the terminal, note that the PORT is selected at random:\n\n```shell\nnREPL server started on port 65110 on host localhost - nrepl://localhost:65110\nnREPL 0.9.0\nClojure 1.11.1\nOpenJDK 64-Bit Server VM 17.0.1+12-39\nInterrupt: Control+C\nExit:      Control+D or (exit) or (quit)\nuser=\u003e\n```\n\nOnce you see the prompt, you can connect your editor to the REPL. We'll go through connecting Calva, but other editors should work similarly.\n\n* Click on the `REPL` button at the bottom left.\n* Select `Connect to a running REPL in your project`\n* Select `deps.edn`\n* Press `enter`, correct port should be detected automatically.\n\nIf everything went well then you should see the following prompt:\n\n```clojure\n; Connecting ...\n; Hooking up nREPL sessions...\n; Connected session: clj\n; TIPS:\n;   - You can edit the contents here. Use it as a REPL if you like.\n;   - `alt+enter` evaluates the current top level form.\n;   - `ctrl+enter` evaluates the current form.\n;   - `alt+up` and `alt+down` traverse up and down the REPL command history\n;      when the cursor is after the last contents at the prompt\n;   - Clojure lines in stack traces are peekable and clickable.\nclj꞉user꞉\u003e \n```\nLet's try starting the server to make sure our application is working.\n\n```clojure\nclj꞉user꞉\u003e (go)\n:initiated\n```\nLet's navigate to `http://localhost:3000/api/health` and see if we have some health check information returned by the server:\n\n```javascript\n{\"time\":\"Fri Feb 10 13:54:36 EST 2023\",\n \"up-since\":\"Wed Jan 18 22:53:21 EST 2023\",\n \"app\":{\"status\":\"up\",\"message\":\"\"}}\n```\n\n### SECTION 2\n\nAt this point you should have your project setup, are able to run and connect to the REPL, and run the web server successfully.\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-1)\n\n### Using Kit Modules\n\nBy now we've synced modules, but what are they? Kit modules consist of templates that can be used to inject code and resources into a Kit project.\n\nBy default, we have the public Kit modules repository linked under the `:modules` key of your `kit.edn` configuration of your project.\n\n```clojure\n{:root         \"modules\"\n :repositories [{:url  \"https://github.com/kit-clj/modules.git\"\n                 :tag  \"master\"\n                 :name \"kit-modules\"}]}\n```\n\nThis configuration says to pull Kit modules template from the repository `https://github.com/kit-clj/modules.git` on the branch `master`.\n\nThis means that it is possible to extend this configuration with private or public modules that you write yourself. We won't be covering this during the workshop, but feel free to give it a try afterwards.\n\n### Using Modules\n\nWe'll need to pull the modules from the remote repository. This is accomplished by running the following commmand in the REPL:\n\n```clojure\nclj꞉user꞉\u003e (kit/sync-modules)\n:done\n```\nIf the command ran successfully then you should see a new `modules` folder in the project containing the modules that were downloaded and are now available for use.\nLet's list the available modules:\n\n```clojure\nclj꞉user꞉\u003e (kit/list-modules)\n:kit/html - adds support for HTML templating using Selmer\n:kit/htmx - adds support for HTMX using hiccup\n:kit/ctmx - adds support for HTMX using CTMX\n:kit/metrics - adds support for metrics using prometheus through iapetos\n:kit/sente - adds support for Sente websockets to cljs\n:kit/sql - adds support for SQL. Available profiles [ :postgres :sqlite ]. Default profile :sqlite\n:kit/cljs - adds support for cljs using shadow-cljs\n:kit/nrepl - adds support for nREPL\n:done\n```\n\nNow, let's use Modules to connect to a database.\n\n### Adding a Database\n\nYou'll notice there is `:kit/sql` module with various **profiles**. Some modules have features that can be chosen from when installing. To use a non-default feature simply specify the feature flag in an options map as a second argument to `kit/install-module`. For example, `{:feature-flag :postgres}`.\n\nNow let's do this to set up PostgreSQL with our project:\n\n```clojure\n(kit/install-module :kit/sql {:feature-flag :postgres})\n```\n\nYou should see something like this in your REPL output\n\n```clojure\nclj꞉user꞉\u003e (kit/install-module :kit/sql {:feature-flag :postgres})\n:kit/sql requires following modules: nil\napplying features to config: [:base]\nupdating file: resources/system.edn\nupdating file: deps.edn\nupdating file: src/clj/io/github/kit/gif2html/core.clj\napplying\n action: :append-requires \n value: [\"[kit.edge.db.sql.conman]\" \"[kit.edge.db.sql.migratus]\"]\n:kit/sql installed successfully!\nrestart required!\n:done\n```\n\nLet's take a look at the change that the module made to the project by looking at the diff:\n\n```shell\ngit diff\n```\nLet's quickly add a `docker-compose.yml`. You can copy this over in to the root of the project:\n\n```yml\nversion: '3.9'\n\nservices:\n  db:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_PASSWORD: gif2html\n      POSTGRES_USER: gif2html\n      POSTGRES_DB: gif2html\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - db_data:/var/lib/postgresql/data\n\nvolumes:\n  db_data:\n```\n\nWe can start this up by running\n\n    docker compose up -d\n\nThis will start the services defined in our `docker-compose.yml` file in detached mode. For more about docker, you can read up on it [here](https://docs.docker.com/compose/).\n\nLet's take a look at some of the code that was generated by installing the SQL module.\n\nFirstly, let's see the dependencies added to our `deps.edn`.\n\n```\nio.github.kit-clj/kit-sql-conman    Conman is a connection pooling library and a utility for setting up HugSQL\nio.github.kit-clj/kit-sql-migratus  Migratus is a library for managing DB migrations\norg.postgresql/postgresql           This is the JDBC PostgreSQL connector\n```\n\nThese dependencies were linked in our `src/clj/io/github/kit/gif2html/core.clj` as `requires` in the namespace.\n\nWe now also have two new directories in our `resources` folder: `migrations` and `sql`. Under `migrations` you will be able to create `.sql` up and down migrations for Migratus to apply to your database schema. And under `sql` there is an empty `queries.sql` file created. Here we will write [HugSQL](https://www.hugsql.org/) queries.\n\nOur `system.edn` was changed to define the Database we'll be using. Here we have three new integrant components defined:\n\n- `:db.sql/connection`: This is the pooled DB connection to our PostgreSQL database\n- `:db.sql/query-fn`: The HugSQL queries defined in your `resources/sql/queries.sql` can be used with this function\n- `:db.sql/migrations`: The configuration for Migratus.\n\nLastly, before we try running our system again, let's change our `:db.sql/connection` connection string in `system.edn`. \n\nHere we have three different profiles. For sake of simplicity, we can use the same DB connection for `test` and `dev`. \n\nLet's drop the `:test` and `:dev` profile, and instead use a `:default` profile with the following value.\n\n```clojure\n:default {:jdbc-url \"jdbc:postgresql://localhost:5432/gif2html?user=gif2html\u0026password=gif2html\"}\n```\n\nNow let's run\n\n```clojure\n(reset)\n```\n\nAnd we should be connected to our database. Let's check that by running\n\n```clojure\n(require '[next.jdbc :as jdbc])\n(jdbc/execute! \n  (:db.sql/connection state/system)\n  [\"select column_name from information_schema.columns where table_name = 'schema_migrations';\"])\n```\n\nIt should return the schema of our migrations table generated by Migratus.\n\nLet's add one more dependency in `deps.edn`:\n\n```clojure\nio.github.kit-clj/kit-postgres {:mvn/version \"1.0.3\"}\n```\n \n This library contains extensions for JDBC to store data structures such as EDN as JSON in PostgreSQL. We can refer it in `core.clj` by adding `[kit.edge.db.postgres]` to the `requires` vector. Let's restart the REPL to make sure the changes take effect.\n\nDon't worry, these commands will be explained shortly.\n\nLet's take a quick look at what we've added above and why. The new component configurations that we now have in `system.edn` will be used to instantiate the Integrant components defined by the libraries that are provided by Kit.\n\nThese libraries are:\n\n* [kit-sql-conman](https://github.com/kit-clj/kit/tree/master/libs/kit-sql-conman)\n* [kit-sql-migratus](https://github.com/kit-clj/kit/tree/master/libs/kit-sql-migratus)\n\nThese libraries declare the multimethods necessary to instantiate the Integrant components. If we look at the `kit-sql-conman` component, it has multimethods such as:\n\n```clojure\n(defmethod ig/init-key :db.sql/connection\n  [_ pool-spec]\n  (conman/connect! pool-spec))\n\n(defmethod ig/halt-key! :db.sql/connection\n  [_ conn]\n  (conman/disconnect! conn))\n```\n\nThese multimethods will accept the map of options that we have declared in `system.edn` under the keyword matching the name of the component declared in the multimethod.\n\n```mermaid\nflowchart TD\n    A[system.edn] --\u003e|define component configuration for Integrant\\nand environment variables| B\n    B[inject environment variables using Aero] --\u003e D\n    C[define Integrant multimethods\\nfor the components declared in system.edn] --\u003e D\n    D[initialize components]\n```\n### SECTION 3\n\nAt this point you should have your database up and running, and your server should be able to connect to it.\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-2)\n\n### Managing the Database\n\nDatabase migrations are a way to manage changes to a database schema while preserving existing data. They are useful because they allow developers to evolve the database schema over time, track and test changes, and collaborate more effectively.\n\nWe can create and execute migrations thanks to [Migratus](https://github.com/yogthos/migratus). Let's create our first migration in the REPL!\n\n```clojure\n(migratus.core/create\n  (:db.sql/migrations state/system)\n  \"create-gif-tables\")\n```\n\nHere Migratus created for us two files, `20230218160207-create-gif-tables.up.sql` and `20230218160207-create-gif-tables.down.sql`.\n\nThe name is identical except for the suffix at the end, `.up.sql` or `.down.sql`. This allows us to express and **up** migration and a **down** migration. The benefit here is if ever you need to revert a migration you can specify the steps to do so.\n\nLet's write our first migration now. What are some database columns you think might be needed for this service? \n\nHere's the one we came up with\n\n```sql\ncreate table if not exists gifs\n(\n  id         serial primary key,\n  ascii      jsonb                     not null,\n  name       text                      not null,\n  created_at timestamptz default now() not null\n);\n```\n\nLet's also write a down migration. To revert, we'll simply drop the table if it exists\n\n```sql\ndrop table if exists gifs;\n```\n\nNow that we have our SQL migrations written out, let's try to execute them with Migratus in our REPL.\n\n```clojure\n(migratus.core/migrate (:db.sql/migrations state/system))\n```\n\nNow if we try that command from before to get our columns from the new `gifs` table, we should see this:\n\n```clojure\nclj꞉user꞉\u003e (jdbc/execute!\n            (:db.sql/connection state/system)\n            [\"select column_name from information_schema.columns where table_name = 'gifs';\"])\n[#:columns{:column_name \"id\"}\n #:columns{:column_name \"created_at\"}\n #:columns{:column_name \"ascii\"}\n #:columns{:column_name \"name\"}]\n```\n\nFor the sake of practice, let's also roll back our migration.\n\n```clojure\n(migratus.core/rollback (:db.sql/migrations state/system))\n```\n\nNow querying for the table columns should return an empty array, `[]`.\n\nIf you ever need to completely roll back all migrations, you can run\n\n```clojure\n(migratus.core/reset (:db.sql/migrations state/system))\n```\n\nWe can also run migrations by simply `(reset)`ing the system, since our `system.edn` has configured migrations to run on startup.\n\nNow we have our initial database schema set up. Next up, we should write some queries for them.\n\n### Querying the Database\n\nFor starters we'll create some simple queries to write and read from our database. We're using HugSQL for writing queries. There's some syntactic sugar we should be aware of, for full docs go to the [HugSQL](https://kit-clj.github.io/docs/database.html#working_with_hugsql) documentation. We'll go over a few below.\n\n```sql\n-- :name create-gif! :\u003c!\n-- :doc inserts and returns a gif\ninsert into gifs(ascii, name)\nvalues (:ascii, :name)\nreturning *;\n\n-- :name get-gif-by-id :? :1\n-- :doc gets a single gif given its ID\nselect *\nfrom gifs\nwhere id = :id;\n\n-- :name list-gifs\n-- :doc lists all gifs\nselect *\nfrom gifs;\n```\n\nLet's `(reset)` again and try out our queries in the REPL.\n\nFirst, let's create an entry. We can create a gif by querying `:create-gif!` and giving it a map with two keys, `:ascii` and `:name`.\n\n```clojure\nclj꞉user꞉\u003e ((:db.sql/query-fn state/system)\n  :create-gif! {:ascii {:blob \"test text\"} :name \"test name\"})\n[{:id 1, :ascii {:blob \"test text\"}, :name \"test name\", :created_at #inst\"2023-02-18T16:25:05.857508000-00:00\"}]\n```\n\nWe can get that gif by querying for its ID in a similar fashion.\n\n```clojure\nclj꞉user꞉\u003e ((:db.sql/query-fn state/system) :get-gif-by-id {:id 1})\n{:id 1, :ascii {:blob \"test text\"}, :name \"test name\", :created_at #inst\"2023-02-18T16:25:05.857508000-00:00\"}\n```\n\nAnd to list all of them we can query with an empty parameter map. Note this argument is required, so even if your query doesn't have any arguments you will need to provide `{}`.\n\n```clojure\nclj꞉user꞉\u003e ((:db.sql/query-fn state/system) :list-gifs {})\n[{:id 1, :ascii {:blob \"test text\"}, :name \"test name\", :created_at #inst\"2023-02-18T16:25:05.857508000-00:00\"}]\n```\n\nWe've been using the `(:db.sql/query-fn state/system)` function quite often for testing. Why not add it to our `user.clj` namespace. Since this component is only available when the system is started, we can either define it in a function, or have it in our rich comment block at the end. We'll do the latter in this example.\n\n```clojure\n(comment\n  (go)\n  (reset)\n  (def query-fn (:db.sql/query-fn state/system)))\n```\n\n### SECTION 4\n\nAt this point you should have a `gifs` table in your database, queries written for it, and able to read and write from the REPL.\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-3)\n\n### Routing\n\nNow that we have a database and queries to store and read the gif data, let's add a couple routes to provide an HTTP API on top of that.\n\nWe'll create a new namespace called `io.github.kit.gif2html.web.controllers.gifs` that will contain the handlers for these operations.\nEach handler will accept the Integrant options along with the HTTP request map as its input and produce an HTTP response as its output.\n\nFirst, let's require `ring.util.http-response` in the namespace declaration so that we can use the response helpers:\n\n```clojure\n(ns io.github.kit.gif2html.web.controllers.gifs\n  (:require\n   [ring.util.http-response :as http-response]))\n```\n\nNext, let's write a handler to save the gif to the database. The handler will grab the `query-fn` from the Integrant options map, and the body parametes from the request. Then it will call `create-gif!` function and pass it the parameters in order to create a record in our db, and return the row that was created as the response.\n\n```clojure\n(defn save-gif [{:keys [query-fn] :as opts}\n                {{{link :link name :name} :body} :parameters}]\n  (-\u003e (query-fn :create-gif! {:ascii {:blob link} :name name})\n      (first)\n      (http-response/ok)))\n```\n\nLet's take a closer look at what's happening here. The `query-fn` is available because we specified it as a referenced by `:reitit.routes/api` in the `resources/system.edn`:\n\n```clojure\n :reitit.routes/api\n {:base-path \"/api\"\n  :env #ig/ref :system/env\n  :query-fn #ig/ref :db.sql/query-fn}\n```\n\nThe map associated with the `:reitit.routes/api` is then accessed by the component declared in `io.github.kit.gif2html.web.routes.api`:\n\n```clojure\n(defmethod ig/init-key :reitit.routes/api\n  [_ {:keys [base-path]\n      :or   {base-path \"\"}\n      :as   opts}]\n  [base-path route-data (api-routes opts)])\n  ```\n\nThe `opts` map contains components that were initialized when the configuration was loaded. The `opts` are then added to the `api-routes` making them available to the handlers:\n\n```clojure\n(defn api-routes [opts]\n  [[\"/swagger.json\"\n    {:get {:no-doc  true\n           :swagger {:info {:title \"io.github.kit.gif2html API\"}}\n           :handler (swagger/create-swagger-handler)}}]\n   [\"/health\"\n    {:get health/healthcheck!}]])\n```\n\nLet's add a new route that will call the `save-gif` handler. First, we'll  have to require the namespace that we made in `io.github.kit.gif2html.web.routes.api`:\n\n```clojure\n(ns io.github.kit.gif2html.web.routes.api\n  (:require\n    ...\n    [io.github.kit.gif2html.web.controllers.gifs :as gifs]))\n```\nNow we can add a new `/gifs` route as follows:\n\n```clojure\n(defn api-routes [opts]\n  [[\"/swagger.json\"\n    {:get {:no-doc  true\n           :swagger {:info {:title \"io.github.kit.gif2html API\"}}\n           :handler (swagger/create-swagger-handler)}}]\n   [\"/health\"\n    {:get health/healthcheck!}]\n   [\"/gifs\"\n    {:post {:summary    \"creates a new gif and returns the inserted row\"\n            :parameters {:body [:map\n                                [:link string?]\n                                [:name string?]]}\n            :responses  {200 {:body [:map\n                                     [:id integer?]\n                                     [:ascii map?]\n                                     [:name string?]]}}\n            :handler    (partial gifs/save-gif opts)}}]])\n```\n\nLet's run `(integrant.repl/reset)` to reload the system and navigate to `http://localhost:3000/api` in order to test out our new route.\n\nLet's head back to the `io.github.kit.gif2html.web.controllers.gifs` namespace where we'll add another route to list all the gifs, and then one to fetch by gif ID. First let's create our controller logic\n\n```clojure\n(defn list-gifs [{:keys [query-fn] :as opts} _]\n  (http-response/ok (query-fn :list-gifs {})))\n\n(defn get-gif-by-id [{:keys [query-fn] :as opts} {{params :path} :parameters}]\n  (http-response/ok (query-fn :get-gif-by-id params)))\n```\n\nWe'll also add a quick Malli definition of our data returned as a Gif in this namespace\n\n```clojure\n(def Gif\n  [:map\n   [:id integer?]\n   [:ascii map?]\n   [:name string?]])\n```\n\nNow to hook this logic into our routes, we can add a `:get` key to our original map on the `/api/gifs` endpoint like so:\n\n```clojure\n[\"/gifs\"\n {:post {:summary    \"creates a new gif and returns the inserted row\"\n         :parameters {:body [:map\n                             [:link string?]\n                             [:name string?]]}\n         :responses  {200 {:body gifs/Gif}}\n         :handler    (partial gifs/save-gif opts)}\n  :get  {:summary   \"returns all created gifs\"\n         :responses {200 {:body [:vector gifs/Gif]}}\n         :handler   (partial gifs/list-gifs opts)}}]\n```\n\nReitit routes are data structures, as you may have noticed in this process. We can nest routes under path segments by creating a vector. For example:\n\n```clojure\n[\"/gifs\"\n  [\"\" ...]\n  [\"/:id\" ...]]\n```\n\nHere we have two valid routes, \"/gifs\" and \"/gifs/:id\". `:id` is a variable path parameter, meaning that any value placed there will be interpreted as the `:id` parameter in our routes. So \"/gifs/23\" would have `{:id 23}` in our path parameters. Important to note, doing this means we cannot have another route on the same level that can potentially conflict with path parameters. For more on this, you can check out the [reitit documentation](https://cljdoc.org/d/metosin/reitit/0.6.0/doc/basics/route-syntax).\n\nWhen you nest routes under path segments, your request handlers cannot be in the top level, i.e. this is why we need the empty `\"\"` route for handling our requests to \"/api/gifs\".\n\nLet's put all of this together, refactoring our existing implementation to follow this pattern.\n\n```clojure\n[\"/gifs\"\n [\"\" {:post {:summary    \"creates a new gif and returns the inserted row\"\n             :parameters {:body [:map\n                                 [:link string?]\n                                 [:name string?]]}\n             :responses  {200 {:body gifs/Gif}}\n             :handler    (partial gifs/save-gif opts)}\n      :get  {:summary   \"returns all created gifs\"\n             :responses {200 {:body [:vector gifs/Gif]}}\n             :handler   (partial gifs/list-gifs opts)}}]\n [\"/:id\" {:get {:summary    \"gets a single gif based off of ID\"\n                :parameters {:path [:map [:id integer?]]}\n                :responses  {200 {:body gifs/Gif}}\n                :handler    (partial gifs/get-gif-by-id opts)}}]]\n```\n\n### SECTION 5\n\nAt this point you should have a `gifs` table in your database, queries written for it, and able to read and write from the REPL.\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-4)\n\n### Adding Dependencies\n\nAt this point we have set up all our scaffolding but we still need to convert gifs to text. To do this we'll use the [gif-to-html](https://github.com/yogthos/gif-to-html) library.\n\nLet's go to that library repository and see how they recommend adding it to your project.\n\nWe can see this project can be added by pasting the following into your `deps.edn`\n\n```clojure\nio.github.yogthos/gif-to-html {:git/tag \"v1.0.0\" :sha \"07fa5d3\"}\n```\n\nYou'll notice this looks quite different than most of our other dependencies in `deps.edn`. This is because there are three different types of dependencies we can reference in our `deps.edn` (two of which we use here):\n\n- Maven repositories (the most common, `{:mvn/version \"1.2.3\"}`)\n- Git repositories (various ways, such as `{:git/tag \"v0.0.1\" :git/sha \"4c4a34d\"}`)\n- Local repositories (a reference to a path, `{:local/root \"../my-lib\"}`)\n\nFor more information you can refer to the [deps and CLI guide](https://clojure.org/guides/deps_and_cli).\n\nOnce you add this dependency to your `deps.edn` you might notice your REPL automatically trying to load in the changes. This doesn't work 100% of the time, so if you do notice any strange issues, please restart your REPL.\n\nOne more library we'll need is [hato](https://github.com/gnarroway/hato), a lightweight wrapper around the Java 11 HTTP client:\n\n```clojure\nhato/hato {:mvn/version \"0.9.0\"}\n```\n\nLet's try these libraries out! We have this sample GIF we can play with `https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif`. In our REPL:\n\n```clojure\n(require '[gif-to-html.convert :as convert])\n(require '[hato.client :as hc])\n(convert/gif-\u003ehtml\n  (:body (hc/get \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" {:as :stream})))\n```\n\nNote: if this URL doesn't work / returns a 404, you could try the image we hosted in this Git repo, i.e. `https://raw.githubusercontent.com/yogthos/kit-workshop/checkpoint-5/banana-dance.gif`\n\n### Creating Integrant Components\n\nHato uses the JDK11 HTTP client in each HTTP request it sends. The best practice for this is to define a client ahead of time and use that client for all relevant requests. While Kit offers the [kit-hato](https://clojars.org/io.github.kit-clj/kit-hato) library, for sake of practice we will build ours from scratch here and hook it up to our routes.\n\nWe'll refer to the documentation in [hato](https://github.com/gnarroway/hato) to find the API to create a client. We'll reference `hato.client` as `hc`. Let's quickly test this in the REPL that we can create a new client.\n\n```clojure\n(def c (hc/build-http-client {}))\n\n(hc/get \"https://www.google.com/\" {:http-client c})\n```\n\n#### Integrant Component Lifecycle\n\nGreat! Now let's make an [Integrant](https://github.com/weavejester/integrant) component from this. We've worked with defining configurations for Integrant before, so now let's create the logic that takes those configurations and creates stateful components from them.\n\nOur system has stateful components that have a lifecycle. This means that when they start up code is executed and an instance of a component is returned. Similarly, on stop or shutdown, code will execute to gracefully bring the instance down. \n\nThere are various Integrant lifecycle multimethod functions. They include:\n\n- `prep-key`: transforms config data (i.e. from your `system.edn`)\n- `init-key`: starts and returns a component instance\n- `halt-key!`: side-effectful, stops the running component instance. Used when shutting down, i.e. would never resume from here.\n- `resume-key`: resumes a component instances. Typically in REPL development\n- `suspend-key!`: side-effectful, stops the running components. Typically in REPL development, since can resume from here\n\nThe difference between halt and suspend is that with suspend you can preserve some component instance state while stopped, whereas with halt the goal is to prepare the component for shutdown.\n\nFor more information on each of these, you can refer to the [Integrant documentation](https://github.com/weavejester/integrant).\n\n#### Compoent Lifecycle Multimethods\n\nWith all that information let's try to create our first Integrant component which returns a Hato HTTP client given a config map. Let's create a new namespace called `io.github.kit.gif2html.components.hato`.\n\n```clojure\n(ns io.github.kit.gif2html.components.hato\n  (:require\n    [integrant.core :as ig]\n    [hato.client :as hc]))\n```\nNext, we'll add the multimethod for starting the client:\n\n```clojure\n(defmethod ig/init-key :http/hato [_ opts]\n  (hc/build-http-client opts))\n```\n\nWhile we don't need to halt the client, for illustration purposes here is how you might stop it:\n\n```clojure\n(defmethod ig/halt-key! :http/hato [_ _http-client]\n  ;; If there was effectful logic here to stop it you would do it here\n  nil)\n```\n\n#### Loading Integrant Components\n\nIn order for Integrant to load the namespace we have to ensure that it is required in the `core` namespace that constitutes the entry point for the application:\n\n```clojure\n[io.github.kit.gif2html.components.hato]\n```\n\n#### Trying Things Out in the REPL\n\nLet's first try it out in our REPL by calling the multimethod `ig/init-key` and ensuring the value we get back is our HTTP client by switching to the `user` namespace and running the following commands:\n\n```clojure\n(reset)\n(ig/init-key :http/hato {})\n=\u003e #object[jdk.internal.net.http.HttpClientFacade 0x25b614f6 \"jdk.internal.net.http.HttpClientImpl@868586f(4)\"]\n```\n\n#### Wiring Up Our New Component\n\nNow let's add this to our `system.edn`. For sake of using the Hato client configuration, let's set a maximum connection timeout to 3 seconds, i.e. `:connect-timeout 3000`\n\n```clojure\n:http/hato\n {:connect-timeout 3000}\n```\n\nNow if we run `(reset)` we should be able to verify that `(:http/hato state/system)` exists.\n\nLet's add this component to our API routes now as `http-client`. It should look something like this:\n\n```clojure\n:reitit.routes/api\n {:base-path   \"/api\"\n  :env         #ig/ref :system/env\n  :query-fn    #ig/ref :db.sql/query-fn\n  :http-client #ig/ref :http/hato}\n```\n\nGreat! So now we have an HTTP client we can use to fetch the GIF and convert it to data.\n\n### Processing GIF Payload\n\nBefore you'll recall we were able to fetch a URL and convert the body stream to an EDN map. Let's do that again, but this time include the link from the parameters, and follow it up by saving the data to the database.\n\n```clojure\n(require '[gif-to-html.convert :as convert]\n         '[hato.client :as hato])\n(let [params {:name \"test\"\n              :link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\"}\n      {http-client :hato/client\n       query-fn :db.sql/query-fn} integrant.repl.state/system]\n\n  (-\u003e\u003e (hato/get\n         (:link params)\n         {:http-client http-client\n          :as          :stream})\n       :body\n       (convert/gif-\u003ehtml)\n       (assoc {:name (:name params)} :ascii)\n       (query-fn :create-gif!)))\n```\n\nWe see that this returns a vector with a map containing the `id` key. We can destructure this and return our newly created map to the frontend as follows:\n\n```clojure\n(ns io.github.kit.gif2html.web.controllers.gifs\n (:require\n  ...\n  [gif-to-html.convert :as convert]\n  [hato.client :as hato]))\n\n(defn save-gif [{:keys [query-fn http-client] :as opts}\n                {{{link :link name :name} :body} :parameters}]\n  (-\u003e\u003e (hato/get\n         link\n         {:http-client http-client\n          :as          :stream})\n       :body\n       (convert/gif-\u003ehtml)\n       (assoc {:name name} :ascii)\n       (query-fn :create-gif!)\n       (first)       \n       (http-response/ok)))\n```\n\n#### Error Handling\n\nHowever, you might ask the question \"what happens if someone sends a broken link?\" Let's try it in the REPL first:\n\n```clojure\n(let [params {:name \"test\"\n              :link \"broken link\"}\n      {http-client :hato/client\n       query-fn :db.sql/query-fn} integrant.repl.state/system]\n  (-\u003e\u003e (hato/get\n         (:link params)\n         {:http-client http-client\n          :as          :stream})\n       :body\n       (convert/gif-\u003ehtml)\n       (assoc {:name name} :ascii)\n       (query-fn :create-gif!)\n       (first)\n       (http-response/ok)))\nExecution error (MalformedURLException) at java.net.URL/\u003cinit\u003e (URL.java:674).\nno protocol: broken link\n```\n\nUh oh, we'll need to handle this case. Let's do that by wrapping the code in a `try`/`catch` block and returning a 500 response if the link is broken.\n\n```clojure\n(defn save-gif [{:keys [query-fn http-client] :as opts}\n                {{{link :link name :name} :body} :parameters}]\n  (try\n    (-\u003e\u003e (hato/get\n           link\n           {:http-client http-client\n            :as          :stream})\n         :body\n         (convert/gif-\u003ehtml)\n         (assoc {:name name} :ascii)\n         (query-fn :create-gif!)\n         (first)\n         (http-response/ok))\n    (catch Exception _e\n      (http-response/internal-server-error))))\n```\n\n#### Swagger API Testing\n\nLet's try this in our Swagger UI. We'll navigate back to our Swagger UI at `http://localhost:3000/api/` and try out the API. Let's first submit a new GIF to be processed by calling the `POST` request with the following payload:\n\n```javascript\n{\n  \"link\": \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\",\n  \"name\": \"banana\"\n}\n```\n\nWe should see a response containing the id of the item that was inserted if the GIF was processed successfully:\n\n```javascript\n{\n  \"id\": 1,\n  \"ascii\": {\n    \"delay\": 100,\n    \"frames\": [...],\n    \"frame-count\": 8\n  },\n  \"name\": \"Bananaaaaas!\"\n}\n```\n\nNext, let's test we are able to retreive the animation by calling the `GET` API. We should see the same result as we did when we inserted a new gif:\n```javascript\n{\n  \"id\": 1,\n  \"ascii\": {\n    \"delay\": 100,\n    \"frames\": [...],\n    \"frame-count\": 8\n  },\n  \"name\": \"Bananaaaaas!\"\n}\n```\n\n### SECTION 6\n\nAt this point you should be comfortable with the following concepts:\n\n* adding dependencies to the project\n* creating new Integrant components by hand\n* updating `system.edn` to wire up new components\n* using Hato HTTP client to fetch binary data\n* error handling with try/catch\n* using Swagger UI to test the endpoints\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-5)\n\n### Testing with the REPL\n\nNow that we've finalized the API, let's try it out using the REPL. Once we've convinced ourselves that everything works the way we expect then we can convert our REPL session into actual tests. First thing we should do is make sure we have a clear REPL state, to do that let's run `(integrant.repl/reset)`. Next, let's try running the functions that service the API with some sample data.\n\nLet's switch the editor to the `io.github.kit.gif2html.web.controllers.gifs` namespace. We will need access to the system in order to run these function. The state of the system can be accessed via `integrant.repl.state/system` any time via the REPL. Since it's a big map, it's often handy to just look at the keys:\n\n```clojure\n(keys integrant.repl.state/system)\n=\u003e (:router/core :db.sql/query-fn :http/hato :reitit.routes/api :handler/ring :server/http :db.sql/connection :router/routes :db.sql/migrations :system/env)\n```\n\nFor our purposes we'll want to grab the `:db.sql/query-fn` and `:http/hato` keys. We can then use these to access the resources that need to be passed in to the functions we want to test. We can now switch to the `io.github.kit.gif2html.web.controllers.gifs` namespace and try to save a GIF using the REPL as follows:\n\n```clojure\n(comment\n  (let [{:keys [:db.sql/query-fn :http/hato] }integrant.repl.state/system]\n    (save-gif {:query-fn query-fn :http-client hato}\n              {:parameters {:body {:link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" :name \"foo\"}}}))\n)\n```\n\nSince we access resources such as the db often, it can be useful to add helper function in the `user` namespace for accessing these resources via the REPL. For example, let's add the following helper:\n\n```clojure\n(defn api-ctx []\n  {:query-fn (:db.sql/query-fn state/system)\n   :http-client (:http/hato state/system)})\n```\n\nNow we can update the test code in the `io.github.kit.gif2html.web.controllers.gifs` namespace to look as follows:\n\n```clojure\n(comment\n  (save-gif (user/api-ctx) {:parameters {:body {:link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" :name \"foo\"}}})\n)\n```\n\nIf everything went well then we should see the name of the animation we just stored in the list of the available GIFs:\n\n```clojure\n(-\u003e\u003e (list-gifs (user/api-ctx) nil)\n      :body\n      (map #(select-keys % [:name :id])))      \n```\n\nLet's try query it directly as well to test our `get-gif-by-id` function:\n\n```clojure\n(get-gif-by-id (user/api-ctx) {:parameters {:path {:id 3}}})\n```\n\nNow we can see that all the fucntion work as intended, and we can take a look at creating actual tests before we move on to the next steps. \n\n### Converting REPL commands into tests\n\nWe'll navigate to the `io.github.kit.gif2html.core-test` namespace. First thing we'll need to do here will be to require the `io.github.kit.gif2html.web.controllers.gifs` namespace as `gifs` which will be testing.\n\nWe'll also use the `system-fixture` from the `io.github.kit.gif2html.test-utils` namespace to get access to the test system:\n\n```clojure\n(ns io.github.kit.gif2html.core-test\n  (:require\n    [io.github.kit.gif2html.test-utils :as utils]\n    [io.github.kit.gif2html.web.controllers.gifs :as gifs]\n    [clojure.test :refer :all]))\n\n(use-fixtures :once (utils/system-fixture))\n```\n\nWe'll also need to write a version of the `dev-ctx` function we added in the `user` namespace for testing:\n\n```clojure\n(defn test-ctx\n  []\n  (let [{:keys [:db.sql/query-fn :http/hato]} (utils/system-state)]\n    {:query-fn    query-fn\n     :http-client hato}))\n```\n\nWith that out of the way, let's try converting the test code from the REPL into tests:\n\n```clojure\n(deftest test-parsing-and-loading-gif\n  (testing \"save GIF\"\n    (let [{:keys [status body]} (gifs/save-gif (test-ctx) {:parameters {:body {:link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" :name \"foo\"}}})]\n      (is (= 200 status))\n      (is (nat-int? (:id body))))))\n```\n\nThe test will save a GIF animation in the db, confirm that the return status is HTTP success, and check that the body contains the `id` that is a number. We can run the test in the REPL the way we'd run a regualr function: `test-parsing-and-loading-gif`. We can also execute all the tests in the namespace as follows:\n\n```clojure\n(run-tests)\n=\u003e {:test 1, :pass 2, :fail 0, :error 0, :type :summary}\n```\n\nLet's update our test to check that we can retrieve the animation:\n\n```clojure\n(deftest test-parsing-and-loading-gif\n  (testing \"save GIF\"\n    (let [{status :status\n           {:keys [id]} :body} (gifs/save-gif (test-ctx) {:parameters {:body {:link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" :name \"foo\"}}})]\n      (is (= 200 status))\n      (is (nat-int? id))\n      (testing \"load GIF\"\n        (let [{:keys [status body]} (gifs/get-gif-by-id (test-ctx) {:parameters {:path {:id id}}})]\n          (is (= 200 status))\n          (is (= id (:id body))))))))\n```\n\nFinally, let's add the test for listing the animations:\n\n```clojure\n(deftest test-parsing-and-loading-gif\n  (testing \"save GIF\"\n    (let [{status :status\n           {:keys [id]} :body} (gifs/save-gif (test-ctx) {:parameters {:body {:link \"https://media.tenor.com/JMzBeLgNaSoAAAAj/banana-dance.gif\" :name \"foo\"}}})]\n      (is (= 200 status))\n      (is (nat-int? id))\n      (testing \"load GIF\"\n        (let [{:keys [status body]} (gifs/get-gif-by-id (test-ctx) {:parameters {:path {:id id}}})]\n          (is (= 200 status))\n          (is (= id (:id body)))))\n      (testing \"list GIFs\"\n        (is (-\u003e (gifs/list-gifs (test-ctx) {}) :body vector?))))))\n```\n\n### Setting up a test DB\n\nUp to now our tests have been running on our development DB. This can be solved by either setting up rollbacks in the context of transactions, or provisioning a separate test database. For our purposes we will set up a test database.\n\nLet's extend our `docker-compose.yml` to include a test database. Note the changed DB port and additional volume:\n\n```yaml\nversion: '3.9'\n\nservices:\n  db:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_PASSWORD: gif2html\n      POSTGRES_USER: gif2html\n      POSTGRES_DB: gif2html\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - db_data:/var/lib/postgresql/data\n  testdb:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_PASSWORD: gif2html\n      POSTGRES_USER: gif2html\n      POSTGRES_DB: gif2html\n    ports:\n      - \"5442:5432\"\n    volumes:\n      - test_db_data:/var/lib/postgresql/data\n\nvolumes:\n  db_data:\n  test_db_data:\n```\n\nAnd also update our `system.edn` to use this new database for test, and keep the original for development.\n\n```clojure\n:db.sql/connection #profile {:dev  {:jdbc-url \"jdbc:postgresql://localhost:5432/gif2html?user=gif2html\u0026password=gif2html\"}\n                              :test {:jdbc-url \"jdbc:postgresql://localhost:5442/gif2html?user=gif2html\u0026password=gif2html\"}\n                              :prod {:jdbc-url #env JDBC_URL}}\n```\n\nWe can test this works by running `docker compose up -d` again in our shell. When we re-run our tests we should see some data populated in the test database.\n\nOf course we want to clear all this test data from our database so we have a clean slate. Since we don't have a seed dataset to reset to, we should just drop all the tables in our target schema (public) and re-run the migrations. We'll add a new function to clear the database in the `test-utils` namespace:\n\n```clojure\n(ns io.github.kit.gif2html.test-utils\n  (:require\n   ...\n   [migratus.core :as migratus]\n   [next.jdbc :as jdbc])\n\n...\n\n(defn clear-db-and-rerun-migrations \n  []\n  (jdbc/execute! (:db.sql/connection (system-state))\n                 [\"do\n$$\n    declare\n        row record;\n    begin\n        for row in select * from pg_tables where schemaname = 'public'\n            loop\n                execute 'drop table public.' || quote_ident(row.tablename) || ' cascade';\n            end loop;\n    end;\n$$;\"])\n  (migratus/migrate (:db.sql/migrations (system-state))))\n```\n\nFinally, we'll update our `system-fixture` to reset the test database state before running each test:\n\n```clojure\n(defn system-fixture\n  []\n  (fn [f]\n    (when (nil? (system-state))\n      (core/start-app {:opts {:profile :test}}))\n    (clear-db-and-rerun-migrations)\n    (f)))\n```\nWe can test that everything still works by running `(run-tests)` again from the `io.github.kit.gif2html.test-utils` namespace.\n\nWhat we did works fine when we have a single fixture, but in many cases we may want to compose multiple fixtures together. We can use `clojure.test/join-fixtures function to do that. Let's see how we can refactor the code above to use multiple fixtures.\n\nFirst, let's add a new require to the\n\n```clojure\n(ns io.github.kit.gif2html.test-utils\n  (:require\n    ...\n    [clojure.test :refer [join-fixtures]]))\n```\n\nNext, let's update the code in `test-utils` as follows:\n\n```clojure\n(defn system-state \n  []\n  (or @core/system state/system))\n\n(defn clear-db-and-rerun-migrations\n  []\n  (jdbc/execute! (:db.sql/connection (system-state))\n                 [\"do\n$$\n    declare\n        row record;\n    begin\n        for row in select * from pg_tables where schemaname = 'public'\n            loop\n                execute 'drop table public.' || quote_ident(row.tablename) || ' cascade';\n            end loop;\n    end;\n$$;\"])\n  (migratus/migrate (:db.sql/migrations (system-state))))\n\n(defn db-fixture [f]\n  (clear-db-and-rerun-migrations)\n  (f))\n\n(defn system-fixture [f]\n  (when (nil? (system-state))\n    (core/start-app {:opts {:profile :test}}))\n  (f))\n\n(def test-fixtures (join-fixtures\n                    [system-fixture\n                     db-fixture]))\n```\n\nOne last thing we'll have to do is to update `use-fixtures` in the `io.github.kit.gif2html.core-test` namespace to use the new fixture:\n\n```clojure\n(use-fixtures :once utils/test-fixtures)\n```\n\nNow that we have this running in our REPL, let's stop our server and try running our tests from the command line:\n\n```shell\nclojure -M:test\n```\n\n### SECTION 7\n\n[Click here to see the solution to the previous section](https://github.com/clojurestream/kit-workshop/tree/checkpoint-6)\n\nNow that our app is working and tested, we're ready to package it for deployment. This is accomplished by building an uberjar using the following command:\n\n```shell\nclojure -T:build all\n```\nOnce the uberjar is built we can run it. We'll first need to set an environment variable for our db:\n\n```shell\nexport JDBC_URL=\"jdbc:postgresql://localhost:5432/gif2html?user=gif2html\u0026password=gif2html\"\n```\nThen we can run the uberjar tha was created in the `target` folder:\n```shell\njava -jar target/gif2html-standalone.jar\n```\nOnce the app starts up, we can navigate back to the Swagger UI page to test that it's working as expected.\n\nFinally, let's try see what the ASCII animation actually looks like. If you have Babashka installed, grab the [ui.clj](https://github.com/clojurestream/kit-workshop/blob/main/ui.clj)\nfile from the repo, and then run it as follows:\n\n```bash\nbb ui.clj\n```\n\nYou can now navigate to `http://localhost:3001/` and try out the service you built from the page.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclojurestream%2Fkit-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclojurestream%2Fkit-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclojurestream%2Fkit-workshop/lists"}