├── .gitignore ├── Procfile ├── README.md ├── bin └── build ├── deps.edn ├── dev └── user.clj ├── project.clj ├── resources └── migrations │ └── 001-todos.edn ├── src └── todo_backend │ ├── core.clj │ ├── handlers.clj │ ├── migration.clj │ └── store.clj └── todobackend.png /.gitignore: -------------------------------------------------------------------------------- 1 | /.cpcache/ 2 | /.nrepl-port 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -cp todo-backend.jar clojure.main -m todo-backend.core $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Todo-Backend in Clojure 2 | ==================== 3 | 4 | This is a implementation of the [Todo-Backend API spec](https://www.todobackend.com/), using Clojure, Ring/Reitit and next-jdbc. 5 | 6 | ![Todo Backend](https://github.com/PrestanceDesign/todo-backend-clojure-reitit/blob/master/todobackend.png) 7 | 8 | The API can be hit directly at [http://todo-backend-clojure-reitit.herokuapp.com/todos](http://todo-backend-clojure-reitit.herokuapp.com/todos) or interactively used with the front end client http://www.todobackend.com/client/index.html?https://todo-backend-clojure-reitit.herokuapp.com/todos 9 | 10 | It also offers a self-hosted OpenAPI documentation, accessible via Swagger UI. 11 | This API is hosted on Heroku here: http://todo-backend-clojure-reitit.herokuapp.com (opens Swagger UI) 12 | 13 | It persists todos to Postgres via [next.jdbc](https://github.com/seancorfield/next-jdbc). 14 | 15 | # Run on localhost 16 | 17 | ## Configure PostgreSQL server 18 | First you must run a Postgres server with Docker for eg.: 19 | 20 | ``` 21 | $ docker run --name some-postgres -e POSTGRES_DB=todos -e POSTGRES_PASSWORD=mypass -d -p 5432:5432 postgres 22 | ``` 23 | 24 | ## Run the application 25 | 26 | ``` 27 | $ export JDBC_DATABASE_URL="jdbc:postgresql://localhost/todos?user=postgres&password=mypass" 28 | $ clj -m todo-backend.core 3000 29 | ``` 30 | 31 | If that port is in use, start it on a different port. For example, port 8100: 32 | 33 | ``` 34 | $ clj -m todo-backend.core 8100 35 | ``` 36 | 37 | # License & Copyright 38 | 39 | Copyright (c) 2020 Michaël SALIHI. 40 | Distributed under the Apache Source License 2.0. 41 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | clojure -A:depstar -m hf.depstar.uberjar todo-backend.jar -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps 3 | {ring/ring-core {:mvn/version "1.8.2"}, 4 | ring/ring-jetty-adapter {:mvn/version "1.8.2"}, 5 | ring-cors/ring-cors {:mvn/version "0.1.13"}, 6 | metosin/reitit {:mvn/version "0.5.9"}, 7 | metosin/muuntaja {:mvn/version "0.6.7"}, 8 | seancorfield/next.jdbc {:mvn/version "1.1.610"} 9 | org.postgresql/postgresql {:mvn/version "42.2.18"} 10 | ragtime/ragtime {:mvn/version "0.8.0"}} 11 | :aliases 12 | {:depstar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}}} 13 | :dev {:extra-paths ["dev"]}}} 14 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [ragtime.jdbc :as jdbc] 3 | [ragtime.repl :as repl])) 4 | 5 | (def config 6 | {:datastore (jdbc/sql-database "jdbc:postgresql:todos?user=postgres&password=mypass") 7 | :migrations (jdbc/load-resources "migrations")}) 8 | 9 | (comment 10 | (repl/migrate config) 11 | (repl/rollback config)) 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/migrations/001-todos.edn: -------------------------------------------------------------------------------- 1 | {:up ["CREATE TABLE todos (id serial primary key, title varchar(255), position int, completed boolean default false);"] 2 | :down ["DROP TABLE todos;"]} 3 | -------------------------------------------------------------------------------- /src/todo_backend/core.clj: -------------------------------------------------------------------------------- 1 | (ns todo-backend.core 2 | (:require [muuntaja.core :as m] 3 | [reitit.coercion.schema :as rcs] 4 | [reitit.ring :as ring] 5 | [reitit.ring.coercion :as rrc] 6 | [reitit.ring.middleware.muuntaja :as rrmm] 7 | [reitit.swagger :as swagger] 8 | [reitit.swagger-ui :as swagger-ui] 9 | [ring.adapter.jetty :as jetty] 10 | [ring.middleware.cors :refer [wrap-cors]] 11 | [schema.core :as s] 12 | [todo-backend.handlers :as todo] 13 | [todo-backend.migration :refer [migrate]] 14 | [todo-backend.store :as store])) 15 | 16 | (defn ok [body] 17 | {:status 200 18 | :body body}) 19 | 20 | (defn append-todo-url [todo request] 21 | (let [host (-> request :headers (get "host" "localhost")) 22 | scheme (name (:scheme request)) 23 | id (:id todo)] 24 | (merge todo {:url (str scheme "://" host "/todos/" id)}))) 25 | 26 | (def app-routes 27 | (ring/ring-handler 28 | (ring/router 29 | [["/swagger.json" {:get 30 | {:no-doc true 31 | :swagger 32 | {:basePath "/" 33 | :info {:title "Todo-Backend API" 34 | :description "This is a implementation of the Todo-Backend API REST, using Clojure, Ring/Reitit and next-jdbc." 35 | :version "1.0.0"}} 36 | :handler (swagger/create-swagger-handler)}}] 37 | ["/todos" {:get {:summary "Retrieves the collection of Todo resources." 38 | :handler todo/list-all-todos} 39 | :post {:summary "Creates a Todo resource." 40 | :handler todo/create-todo} 41 | :delete {:summary "Removes all Todo resources" 42 | :handler todo/delete-all-todos} 43 | :options (fn [_] {:status 200})}] 44 | ["/todos/:id" {:parameters {:path {:id s/Int}} 45 | :get {:summary "Retrieves a Todo resource." 46 | :handler todo/retrieve-todo} 47 | :patch {:summary "Updates the Todo resource." 48 | :handler (fn [{:keys [parameters body-params] :as req}] (-> body-params 49 | (store/update-todo (get-in parameters [:path :id])) 50 | (append-todo-url req) 51 | ok))} 52 | :delete {:summary "Removes the Todo resource." 53 | :handler (fn [{:keys [parameters]}] (store/delete-todos (get-in parameters [:path :id])) 54 | {:status 204})}}]] 55 | {:data {:muuntaja m/instance 56 | :coercion rcs/coercion 57 | :middleware [rrmm/format-middleware 58 | rrc/coerce-exceptions-middleware 59 | rrc/coerce-response-middleware 60 | rrc/coerce-request-middleware 61 | [wrap-cors :access-control-allow-origin #".*" 62 | :access-control-allow-methods [:get :put :post :patch :delete]]]}}) 63 | (ring/routes 64 | (swagger-ui/create-swagger-ui-handler {:path "/"})) 65 | (ring/create-default-handler 66 | {:not-found (constantly {:status 404 :body "Not found"})}))) 67 | 68 | (defn -main [port] 69 | (migrate) 70 | (jetty/run-jetty #'app-routes {:port (Integer. port) 71 | :join? false})) 72 | 73 | (comment 74 | (def server (jetty/run-jetty #'app-routes {:port 3000 75 | :join? false}))) 76 | -------------------------------------------------------------------------------- /src/todo_backend/handlers.clj: -------------------------------------------------------------------------------- 1 | (ns todo-backend.handlers 2 | (:require [ring.util.response :as rr] 3 | [todo-backend.store :as store])) 4 | 5 | (defn append-todo-url [todo request] 6 | (let [host (-> request :headers (get "host" "localhost")) 7 | scheme (name (:scheme request)) 8 | id (:id todo)] 9 | (merge todo {:url (str scheme "://" host "/todos/" id)}))) 10 | 11 | (defn list-all-todos [request] 12 | (-> #(append-todo-url % request) 13 | (map (store/get-all-todos)) 14 | rr/response)) 15 | 16 | (defn create-todo [{:keys [body-params] :as request}] 17 | (-> (store/create-todos body-params) 18 | (append-todo-url request) 19 | rr/response)) 20 | 21 | (defn delete-all-todos [_] 22 | (store/delete-all-todos) 23 | (rr/status 204)) 24 | 25 | (defn retrieve-todo [{:keys [parameters] :as request}] 26 | (let [id (-> parameters :path :id)] 27 | (-> (store/get-todo id) 28 | (append-todo-url request) 29 | rr/response))) 30 | -------------------------------------------------------------------------------- /src/todo_backend/migration.clj: -------------------------------------------------------------------------------- 1 | (ns todo-backend.migration 2 | (:require [ragtime.jdbc :as jdbc] 3 | [ragtime.repl :as repl])) 4 | 5 | (def config 6 | {:datastore (jdbc/sql-database (System/getenv "JDBC_DATABASE_URL")) 7 | :migrations (jdbc/load-resources "migrations")}) 8 | 9 | (defn migrate [] 10 | (repl/migrate config)) 11 | -------------------------------------------------------------------------------- /src/todo_backend/store.clj: -------------------------------------------------------------------------------- 1 | (ns todo-backend.store 2 | (:require [clojure.set :refer [rename-keys]] 3 | [next.jdbc :as jdbc] 4 | [next.jdbc.result-set :as rs] 5 | [next.jdbc.sql :as sql])) 6 | 7 | (def ds (jdbc/get-datasource (System/getenv "JDBC_DATABASE_URL"))) 8 | 9 | (def db (jdbc/with-options ds {:builder-fn rs/as-unqualified-lower-maps})) 10 | 11 | (defn as-row [row] 12 | (rename-keys row {:order :position})) 13 | 14 | (defn as-todo [row] 15 | (rename-keys row {:position :order})) 16 | 17 | (defn create-todos [todo] 18 | (as-todo (sql/insert! db :todos (as-row todo)))) 19 | 20 | (defn get-todo [id] 21 | (as-todo (sql/get-by-id db :todos id))) 22 | 23 | (defn update-todo [body id] 24 | (sql/update! db :todos (as-row body) {:id id}) 25 | (get-todo id)) 26 | 27 | (defn delete-todos [id] 28 | (sql/delete! db :todos {:id id})) 29 | 30 | (defn get-all-todos [] 31 | (jdbc/execute! db ["SELECT * FROM todos;"])) 32 | 33 | (defn delete-all-todos [] 34 | (sql/delete! db :todos [true])) 35 | -------------------------------------------------------------------------------- /todobackend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prestancedesign/todo-backend-reitit/14fc57490fe03ac67d9e4ffd70eb129841640b47/todobackend.png --------------------------------------------------------------------------------