Unifying server and client validation using Clojure[Script] and Clova.
clova supports both Clojure and ClojureScript, in this blog post we will create a simple Compojure server that validates a request and we will apply that same validation on the client side.
I'll be pushing all the code for this post to here.
Getting started
Let's start by creating a new project using lein
.
lein new compojure clova-blog-post
Once we've done that let's make sure it compiles...
lein compile :all
.. and that the server starts up.
lein ring server
You should see a new window in your default browser open and load http://localhost:3000
, displaying "Hello World".
The current state of the project should match the v0.0.1 tag.
Now that's out of the way, let's pull in the clova dependency. Open project.clj
in your editor of choice (it's vim right?).
Once you've opened project.clj
add [clova "0.26.0"]
to :dependencies
and run:
lein compile :all
Next, open src/clova_blog_post/handler.clj
and :require
clova.core.
Our first validation set
For this blog post we will handle the POST
of a simple to-do list model, let's declare the validation set for it.
Validation sets are just sequences of keys followed by the functions that validate the key. Where a function requires arguments, such as the length value passed to shorter?
it is wrapped in a sequence. Where map traversal needs to take place to get to the key to validate then we also wrap the key in a sequence, as in the [:meta :category]
example.
In this validation set we make use of required?
, stringy?
, shorter?
, longer?
and date?
.
By default clova is somewhat relaxed about it's validation. Unless explicitly told by the use of required?
all keys are considered to be optional.
stringy?
does what it says on the tin, checks for the presence of a string like value.
shorter?
works on strings and sequences and checks that a given value has a length shorter than X.
longer?
works on strings and sequences and checks that a given value has a length longer than X.
date?
checks an input value to see if it is a date.
Now, let's compile again.
lein compile :all
At this point you will probably get an exception.
Exception in thread "main" java.lang.RuntimeException: No such var: c/equal?, compiling:(clova/core.cljc:267:7)
This is because clova makes use of clj-time, as does Compojure. Unfortunately Compojure pulls in an old version without the equal?
function. We can fix this by excluding clj-time when we reference Compojure, like so:
Notice the :exclusions
key for Compojure.
After this run:
lein clean
lein compile :all
Everything should be good again now. The current state of the project should match the v0.0.2 tag.
Now, let's setup handler.clj
for JSON requests/responses and enable CORS. To start with we need to add [ring/ring-json "0.4.0"]
to our :dependencies
in project.clj
, then we update our ns
declaration in handler.clj
to refer to handler
, response
, wrap-json-response
and wrap-json-params
, like so:
Still editing handler.clj
we create a couple of middleware functions to support CORS.
Then we define our app
to make use of both the middleware functions and our imported JSON functions.
We can now remove the default GET route:
Replacing it with a POST route for our to-do item.
Here, we validate the payload using the to-do-validation
set we created earlier. If it is valid?
we return the payload and a 200 status code, if it is not valid?
we return the :results
of the validation and a 400 status code.
Testing the server
If we make a valid request to our handler, like so:
We should see the payload in the response:
If we make an invalid request to our handler by passing an invalid date value, like so:
We should see the validation results in the response:
With our server side validation working we can now move onto the client side. The state of the project currently should match the v0.0.3 tag
Client side
Clojure 1.7 introduced reader conditionals, these allow us to reuse as much code as possible across the various Clojure platforms.
In order to make our validation logic useable on the Clojure and ClojureScript platform we need to move it to a *.cljc
file, which is loadable by both Clojure and ClojureScript, let's do it.
First we need to create validation.cljc
.
vim src/clova_blog_post/validation.cljc
Then, let's move our to-do-validation
definition to that file. It should look like this:
We need to update handler.clj
to refer to to-do-validation
, we can add the following to:require
.
[clova-blog-post.validation :refer [to-do-validation]]
Just to verify our changes we can restart the server and run our curl
tests again.
With that out of the way we can open project.clj
and add the ClojureScript dependency by adding [org.clojure/clojurescript "1.8.51"]
to :dependencies
.
If you are new to ClojureScript it is worth checking out the Quick Start wiki article. We will start by getting a generic "Hello world" to print to the browser console.
First, let's bootstrap the ClojureScript build.
vim src/clova_blog_post/build.clj
This is pretty self explanatory, we give it the path the build, the output path and specify a main entry point clova-blog-post.client
(which we will define later).
I usually wrap this with a shell script.
Now we can create the clova-blog-post.client
entry point:
vim src/clova_blog_post/client.cljs
All we do here is enable console printing and write "Hello world", with that done we can create index.html
and reference our compiled JavaScript.
vim index.html
If you open index.html
in your browser of choice you should see "Hello world" output to the developer console. (party)
Finally, let's hook up our validation.
vim index.html
Here we add a few text fields to build our to-do and then a text area to dump our validation results to.
Now let's validate those text fields.
vim src/clova_blog_post/client.cljs
Here we pull out the values from our text boxes and build up our to-do map. Then we use exactly the same validation as we did on the server side (to-do-validation
) and dump the results to our text area.
Rounding up
I'm going to cut this short as it's already gone on way too long and we have worked through a simple example of using the same validation logic on the client and the server, even if we've not implemented any interaction between them, the principal remains the same.Obviously this is a very simple example, we can handle much more complex validation and give a rich experience on the client side while still keeping the integrity of our data using server side validation. All this with as much re-use as possible for our core validation logic.
The state of the project currently should match the v0.0.4 tag