├── db ├── reset.sql └── friends-table.sql ├── preview.gif ├── post.sh ├── .vscode └── settings.json ├── db_reset.sh ├── doc └── intro.md ├── .env.development ├── test └── clojure_example │ └── core_test.clj ├── .gitignore ├── db_create.sh ├── src └── clojure_example │ ├── lib │ ├── api.clj │ ├── db.clj │ └── routes.clj │ └── core.clj ├── README.md ├── project.clj └── LICENSE /db/reset.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.friends; 2 | DROP SEQUENCE public.friends_id_seq; -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kenreilly/clojure-example/HEAD/preview.gif -------------------------------------------------------------------------------- /post.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | curl localhost:3000/friends -X POST -H "Content-Type: application/json" -d $1 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": false 5 | } -------------------------------------------------------------------------------- /db_reset.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | psql clojure_pg_example -f db/reset.sql 3 | psql clojure_pg_example -f db/friends-table.sql -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to clojure_example 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DB_PORT=5432 3 | DB_HOST=localhost 4 | DB_USER=clojure_pg_user 5 | DB_PASS=insecure_password 6 | DB_NAME=clojure_pg_example -------------------------------------------------------------------------------- /test/clojure_example/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-example.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-example.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | .nrepl-port 14 | .cpcache/ 15 | 16 | .env.* 17 | !.env.development -------------------------------------------------------------------------------- /db_create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | psql postgres -c "CREATE USER clojure_pg_user WITH LOGIN SUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION PASSWORD 'insecure_password'" 3 | psql postgres -c "CREATE DATABASE clojure_pg_example WITH OWNER = clojure_pg_user ENCODING = 'UTF8' CONNECTION LIMIT = -1;" 4 | psql clojure_pg_example -f db/friends-table.sql -------------------------------------------------------------------------------- /src/clojure_example/lib/api.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-example.lib.api 2 | (:require 3 | [compojure.core :refer :all] 4 | [compojure.route :as route] 5 | [clojure.pprint :as pp] 6 | [clojure.string :as str] 7 | [clojure.data.json :as cjson] 8 | [clojure-example.lib.db :as db]) 9 | (:gen-class)) 10 | 11 | (defn get-friends 12 | "Retrieve a list of records from friends" 13 | [] 14 | (db/select :friends [:id :name :nickname :occupation])) 15 | 16 | (defn add-friend 17 | "Add a record to friends" 18 | [{ name :name nickname :nickname occupation :occupation :as record }] 19 | (db/insert :friends record)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure-example 2 | Example project demonstrating how build a simple REST API with Clojure 3 | 4 | ![](preview.gif) 5 | 6 | ## Getting Started 7 | To run this project: 8 | * Have [Clojure](https://clojure.org) installed 9 | * Clone this repo 10 | * `cd` into project directory 11 | 12 | ## Testing the REST API 13 | To test out the REST API: 14 | * Start the server with `lein run server` 15 | * Add friends with `./post.sh '{"name":"joe","nickname":"beers","occupation":"bartender"}'` 16 | * Retrieve list of friends with `curl localhost:3000/friends` 17 | 18 | ## Contributions 19 | Suggestions, corrections and other ideas are welcome. Thanks! -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure_example "0.1.0-SNAPSHOT" 2 | :description "An example project demonstrating how to build a REST API in Clojure" 3 | :url "https://innovationgroup.tech" 4 | :license {:name "MIT" 5 | :url "https://github.com/kenreilly/clojure-example/blob/master/LICENSE"} 6 | :dependencies [ 7 | [org.clojure/clojure "1.10.0"] 8 | [org.clojure/data.json "0.2.6"] 9 | [org.clojure/java.jdbc "0.7.10"] 10 | [org.postgresql/postgresql "42.1.4"] 11 | [ring/ring-defaults "0.3.2"] 12 | [ring/ring-devel "1.6.3"] 13 | [ring/ring-json "0.5.0"] 14 | [compojure "1.6.1"] 15 | [http-kit "2.3.0"] 16 | [lynxeyes/dotenv "1.0.2"]] 17 | :main ^:skip-aot clojure-example.core 18 | :target-path "target/%s" 19 | :profiles {:uberjar {:aot :all} :dev {:main clojure-example.core/-dev-main}}) -------------------------------------------------------------------------------- /src/clojure_example/lib/db.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-example.lib.db 2 | (:require 3 | [dotenv :refer [env app-env]] 4 | [clojure.java.jdbc :as jdbc] 5 | [clojure.pprint :as pp] 6 | [clojure.string :as str]) 7 | (:gen-class)) 8 | 9 | (def -db 10 | {:dbtype "postgresql" 11 | :dbname (env :DB_NAME) 12 | :host (env :DB_HOST) 13 | :user (env :DB_USER) 14 | :password (env :DB_PASS)}) 15 | 16 | (defn concat-fields 17 | "Concat field names for SQL" 18 | [fields] 19 | (clojure.string/join ", " (map name fields))) 20 | 21 | (defn insert 22 | "Insert a record into a table" 23 | [table record] 24 | (first (jdbc/insert! -db table record))) 25 | 26 | (defn select 27 | "Select records from a table" 28 | [table fields] 29 | (jdbc/query -db [(str "select " (concat-fields fields) " from " (name table))] )) -------------------------------------------------------------------------------- /src/clojure_example/lib/routes.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-example.lib.routes 2 | (:require 3 | [compojure.core :refer :all] 4 | [compojure.route :as route] 5 | [clojure.pprint :as pp] 6 | [clojure.string :as str] 7 | [clojure.data.json :as json] 8 | [clojure-example.lib.api :as api]) 9 | (:gen-class)) 10 | 11 | (defn echo-route 12 | "Echo back the request" 13 | [req] 14 | {:status 200 15 | :headers {"Content-Type" "text/html"} 16 | :body (-> (str "GET '/' " req))}) 17 | 18 | (defn get-friends-route 19 | "Echo back a name" 20 | [req] 21 | {:status 200 22 | :headers {"Content-Type" "application/json"} 23 | :body (-> (api/get-friends))}) 24 | 25 | (defn add-friend-route 26 | "Endpoint for adding a friend" 27 | [req] 28 | {:status 200 29 | :headers {"Content-Type" "application/json"} 30 | :body (-> (api/add-friend (req :params)))}) -------------------------------------------------------------------------------- /db/friends-table.sql: -------------------------------------------------------------------------------- 1 | -- SEQUENCE: public.friends_id_seq 2 | 3 | -- DROP SEQUENCE public.friends_id_seq; 4 | 5 | CREATE SEQUENCE public.friends_id_seq; 6 | 7 | ALTER SEQUENCE public.friends_id_seq 8 | OWNER TO clojure_pg_user; 9 | 10 | 11 | -- Table: public.friends 12 | 13 | -- DROP TABLE public.friends; 14 | 15 | CREATE TABLE public.friends 16 | ( 17 | id integer NOT NULL DEFAULT nextval('friends_id_seq'::regclass), 18 | name character varying(16) COLLATE pg_catalog."default" NOT NULL, 19 | nickname character varying(16) COLLATE pg_catalog."default", 20 | occupation character varying(32) COLLATE pg_catalog."default", 21 | create_timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | CONSTRAINT friends_pkey PRIMARY KEY (id) 23 | ) 24 | WITH ( 25 | OIDS = FALSE 26 | ) 27 | TABLESPACE pg_default; 28 | 29 | ALTER TABLE public.friends 30 | OWNER to clojure_pg_user; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kenneth Reilly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/clojure_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-example.core 2 | (:require 3 | [dotenv :refer [env app-env]] 4 | [compojure.core :refer :all] 5 | [compojure.route :as route] 6 | [org.httpkit.server :as server] 7 | [ring.middleware.json :as js] 8 | [ring.middleware.defaults :refer :all] 9 | [ring.middleware.reload :refer [wrap-reload]] 10 | [clojure.pprint :as pp] 11 | [clojure.string :as str] 12 | [clojure.data.json :as json] 13 | [clojure-example.lib.routes :as routes]) 14 | (:gen-class)) 15 | 16 | (defroutes app-routes 17 | (GET "/" [] routes/echo-route) 18 | (GET "/friends" [] routes/get-friends-route) 19 | (POST "/friends" [] routes/add-friend-route)) 20 | 21 | (defn -main 22 | "Production" 23 | [& args] 24 | (let [port (Integer/parseInt (env :PORT))] 25 | (server/run-server (js/wrap-json-params (js/wrap-json-response (wrap-defaults #'app-routes api-defaults))) {:port port}) 26 | (println (str "Running webserver at http:/127.0.0.1:" port "/")))) 27 | 28 | (defn -dev-main 29 | "Dev/Test (auto reload watch enabled)" 30 | [& args] 31 | (let [port (Integer/parseInt (env :PORT))] 32 | (server/run-server (wrap-reload (js/wrap-json-params (js/wrap-json-response (wrap-defaults #'app-routes api-defaults)))) {:port port}) 33 | (println (str "Running webserver at http:/127.0.0.1:" port "/")))) --------------------------------------------------------------------------------