-
Notifications
You must be signed in to change notification settings - Fork 149
Tutorial
compojure-api
provides a powerful and easy to use way to build web APIs declaratively, with all kinds of fun stuff for free, like Schema validation and interactive Swagger docs. We're going to get started by building a simple API for rolling dice on the internet.
compojure-api
provides a handy Leiningen template, so to start off with, let's run lein:
lein new compojure-api dice-api
If we navigate into our newly created dice-api
directory, it should look something like this:
.
├── README.md
├── project.clj
└── src
└── dice_api
└── handler.clj
Let's open up handler.clj
in our editor and take a look at what we start off with.
(ns dice-api.handler
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[schema.core :as s]))
(s/defschema Pizza
{:name s/Str
(s/optional-key :description) s/Str
:size (s/enum :L :M :S)
:origin {:country (s/enum :FI :PO)
:city s/Str}})
(def app
(api
{:swagger
{:ui "/"
:spec "/swagger.json"
:data {:info {:title "Dice-api"
:description "Compojure Api example"}
:tags [{:name "api", :description "some apis"}]}}}
(context "/api" []
:tags ["api"]
(GET "/plus" []
:return {:result Long}
:query-params [x :- Long, y :- Long]
:summary "adds two numbers together"
(ok {:result (+ x y)}))
(POST "/echo" []
:return Pizza
:body [pizza Pizza]
:summary "echoes a Pizza"
(ok pizza)))))
That's a lot of stuff, and a good starting example in and of itself. We can see already an example of a lot of the stuff compojure-api
can do. You can see, for instance, the Pizza
schema, which we've used to define both an incoming and outgoing query schema for out /echo
path.
Let's see how this all works. In your shell, run the following command to fire up the server:
lein ring server
Once it's started, it should open up a browser tab right to the Swagger docs, but if not, you can go to http://localhost:3000/index.html in your browser. It should look something like this:
Not very exciting yet, so we click "Show/Hide", and voila, it's our API!
This is helpfully autogenerated from us from the API we declared. You can click around to play with the template API, in particular, try calling the /echo
path with the example input, then try again with one of the keys removed. Hooray for schema validated APIs! Next, we'll get to making our own API.
To get started on making our own API, let's start by clearing away some of the sample code, so we have a clean slate to work with. remove the Pizza
schema and the two GET/POST routes, so that you have a bare frame of our API context, like so:
(ns dice-api.handler
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[schema.core :as s]))
(def app
(api
{:swagger
{:ui "/"
:spec "/swagger.json"
:data {:info {:title "Dice-api"
:description "Compojure Api example"}
:tags [{:name "api", :description "some apis"}]}}}
(context "/api" []
:tags ["api"]
)))
Now since we're creating a server to roll dice, the simplest starting route should be that: a route that returns the roll of a die. Now there's lots of different ways we could structure our data model for this, but let's follow the principle of "start simple, then grow."
Since we're in a JSON world now, let's make a simple schema for our return values:
(s/defschema Result
{:result s/Int})
Now under our /api
context, we'll make a simple base route, /roll
that rolls a six-sided die and returns it as a Result
.
(GET "/roll" []
:return Result
:summary "Rolls a six sided die"
(ok {:result (inc (rand-int 6))}))
Now you can go over to your browser and give it a try, just head to [localhost:3000/api/roll] and you should see in your browser a JSON response containing a random number. You can also open up the Swagger UI and try out different response types like EDN or transit+json.
Let's walk through the components of a route declaration line by line:
(GET "/roll" []
Here we start by calling GET
, which is a macro that creates a route handler function. GET takes a variable series of arguments, starting with a string that defines the URI of the route within the current context (/api
in our case, inherited from the parent context). This string can also contain identifiers for route parameters, but we'll get to that later.
The second argument that appears to be a map is actually a destructuring of the request body. We don't need to do any of this manually right now, but you can find out more about this in Routes in Detail.
:return Result
:summary "Rolls a six sided die"
After the URI and requests, a route takes a series of key-value arguments that let us do useful things like describe return and request schemas, and so forth. Here we have :return
, which tells the route what Schema to check against our response body (in our case, the Result
we defined earlier), and a :summary
, which lets us give a helpful doc string description that's used by Swagger UI.
(ok {:result (inc (rand-int 6))}))
Finally the last part of a route is a body expression which needs to return a HTTP response. In our case, we're returning a simple OK
, with a body containing a result map with our random die roll.