git clone ''

(ql:quickload :WorksHub.leona)


Pen or sword - the shield is mightiest - Leona

Clojars Project CircleCI

A toolbox designed to make working with GraphQL and clojure.spec a more pleasant experience.

Leona can build Lacinia schema just by telling it the queries and mutations you want to make. You can add resolvers for specific fields and add middleware inside the executor.

Quick Usage

(require '[leona.core :as leona])

(let [schema (-> (leona/create)
                 (leona/attach-query ::query-spec ::object query-resolver-fn)
                 (leona/attach-mutation ::mutation-spec ::object mutator-fn)
                 (leona/attach-field-resolver ::field-in-object field-resolver-fn)
                 (leona/attach-middleware middeware-fn)
  (leona/execute schema "query { object(id: 1001) { id, name, field_in_object }}")



To add a query to the schema use attach-query:

(-> (leona/create)
    (leona/attach-query ::query-spec ::object query-resolver-fn))

::query-spec is a spec for the GraphQL query, ::object is the spec for the returned data, and query-resolver-fn is the resolver function that will fetch and return the data.


Mutations are very similar to queries. To add a mutation to the schema use attach-mutation:

(-> (leona/create)
    (leona/attach-mutation ::mutation-spec ::object mutator-fn))

::mutation-spec is a spec for the GraphQL mutation, ::object is the spec for the returned data, and mutator-fn is the function that will mutate the existing data and return the new, mutated data.

Field Resolvers

To provide a resolver for a specific field, use attach-field-resolver:

(-> (leona/create)
    (leona/attach-query ::query-spec ::object query-resolver-fn)
    (leona/attach-field-resolver ::field-in-object field-resolver-fn)

::field-in-object is a spec for the field in an existing object. It must match a field already being inserted, in either a query or mutation. If the field isn't found amongst the objects in the schema then it won't be inserted. field-resolver-fn is a resolver fn for that specific field. As is true of all field resolvers, it will be called after the root query/mutation resolver, so the value arg will already have data in it. The field resolver should add to this value.


It might be useful to add middleware inside the Lacinia executor e.g. you want to inspect a query/mutation prior to resolving or you want to inspect a value before it's passed back to Lacinia.

(-> (leona/create)
    (leona/attach-middleware middeware-fn-1)
    (leona/attach-middleware middeware-fn-2))

Middleware functions are applied in the order that they are attached and take 4 args:

(defn middleware-fn-1
[handler ctx query value]
  ;; do something

(defn middleware-fn-2
[handler ctx query value]
  (let [result (handler)]
  ;; do something

Middleware should call (handler) if they intend to allow the process to continue. Currently the ctx, query and value args should not be passed into handler as they cannot be overridden by the middleware.

In order to use the middleware you'll need to use leona's execute fn:

(leona/execute compiled execute-string)

compiled is the output of (leona/compile) and execute-string is a GraphQL query/mutation/etc.

Custom Scalars

Custom scalars are also supported.

(-> (leona/create)
    (leona/attach-custom-scalar ::date {:parse     #(tf/parse (tf/formatters :date-time) %)
                                        :serialize #(tf/unparse (tf/formatters :date-time) %)}))

Anywhere the ::date spec is referenced by an object, query or mutation, the parse and serialize fns will be used to transform the data. Be aware, however; whilst Leona will still perform its own internal spec validation, Lacinia will not perform validation for over-the-wire values. It's therefore important that your parse fn can handle incorrect data (unlike the one in the example!) and will still return something valid to Leona.


If your spec cannot be inferred (automatically converted into an accurate schema) you can always override the inferred type by using the spec function from spec-tools:

(require '[spec-tools.core :as st])

(s/def ::object (st/spec object? {:type 'String}))

If you'd like to add a description to the schema you can also use the spec function:

(s/def ::object (st/spec string? {:description "This is my object}))

Sometimes, you may want to add a custom object that’s not referred to in any of your queries, mutations or field resolvers (e.g., if you want to refer to it from an external schema attached via attach-schema). For this use case, Leona provides attach-object:

(-> (leona/create)
    (leona/attach-object :some/object :input? true))

If you pass :input?, as in the example above, Leona will generate an input object (named object_input) in addition to an ordinary object.

Type Aliases

If you're working with a large amount of legacy specs, sometimes you can have name clashes that aren't easy to resolve. To help with this you can use ‘type aliases’ which will automatically replace instances of type names wherever they are used.

(-> (leona/create)
    (leona/attach-type-alias :my.ns/type :mytype)

In this example, if my.ns/type is an object, the corresponding object would be created as :mytype instead, and any references to my.ns/type would automatically be updated to use the alias instead. Note, this doesn't refer the field names, just the types.


The ordering of attach-* fns does not matter, other than for middleware.


Copyright © 2018 Antony Woods, WorksHub Ltd.

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.