No rabbits, just Clojure

Tadam Web Framework

Start

Tadam is a minimalistic framework for creating dynamic function-oriented websites. All the potential of Clojure simplified for fast development. It could be in someway equivalent to Flask, but a little more modern.

The rest of the magic will come from you.

Order list

urls.clj

(defroutes public
  (GET "/" [] view-public/index))

views.clj

(defn index
  [req]
  (render-HTML req "welcome.html"
    { :name "Tadam"
      :time (System/currentTimeMillis) }))

welcome.html

<h1>Welcome to {{ name }} Framework</h1>
<p>No bugs since: {{ time }}</p>

🎉 TADAM!!! 🎉

<h1>Welcome to Tadam Framework</h1>
<p>No bugs since: 1590773922089</p>

Installation

1. Make sure you have Openjdk or Oracle-jdk installed.

Debian/Ubuntu

sudo apt install default-jdk

Mac OS

brew install openjdk

2. Install Clojure and Leiningen.

Debian/Ubuntu

sudo apt install clojure leiningen

Mac OS

brew install clojure/tools/clojure leiningen

3. Create a 🎩Tadam🎩 project.

lein new tadam-lite myproject

4. Run.

cd myproject
lein run

Great 🎉! 🔥 Your own web server 🔥 is up and running!

Now open your browser.

localhost:7404

Directory Structure

When you generate your project you will find the following structure.

config.yaml
project.clj
README.md
resources/
    public/
        css/
            main.css
        img/
            tadam.svg
        js/
            main.js
    templates/
        layouts/
            base.html
        public/
            404.html
            welcome.html
src/
    myproject/
        views/
            public.clj
        config.clj
        core.clj
        urls.clj

Let's see the description of the relevant sections.

Root

Name Type Description
config.yaml File The configuration of your application, to which you can add all the variables you need. By default you will find domain, debug and port. You can see more information in Configuration
project.clj File Clojure configuration. Add as many libraries as you need.
README.md File Example of README.md file for your project.

Resources folder

Everything related to the template system or static files (javascript, images, styles...).

Name Type Description
public Folder Static material.
templates Folder Templates HTML.
templates/layouts/base.html File Example of a template that will contain all the structure that will not change between pages, such as the header or footer.
templates/public/welcome.html File Sample HTML page.

src folder

Source code in Clojure, the heart of the beast. System of routes, views, business logic...

Name Type Description
views/ Folder The views are invoked by the urls.clj . When a route is visited, a function within the appropriate view is called.
views/public.clj File Example of a public view. In the future it should grow with other private, management, identification or APIs.
config.clj File Place to store the configuration. You can configure as many variables as you need or add them as root in config.yaml
core.clj File First file to be executed in your application. It must have the minimum to start.
urls.clj File Routes of your website.

Configuration

By default there’ll be a config.yaml file like the one below.

domain: "http://localhost"
debug: true
port: 7404
  • domain: Defines the domain of your application (http://example.com). If you’re running locally or have a reverse proxy, there’s no need to change it.
  • debug: If true, then automatic code refreshes are enabled and CORS is ignored. Otherwise, it is assumed that you are in a production environment and CORS is blocked based on the domain.
  • port: Port to use.

Feel free to add as many variables as you need.

For example, let's create a wizard variable and use it.

config.yaml

domain: "http://localhost"
debug: true
port: 7404
wizard: Merlin

core.clj

(ns myproject.core
  (:require
    [myproject.config :refer [config]]))

(defn -main [& args]
  ;; Main
  (prn (config :wizard)))
" Merlin

Routing

Simple route

Inside urls.clj you can find an example where 2 routes are declared and linked to their respective views.

If you want to add new routes you should follow 4 steps.

  1. Import the View.
  2. Use or create a group of Routes.
  3. Define the Routes using View.
  4. Add your group to the set of all Routes (optional, only if it doesn't exist). Always leave route resources used for static content at the end.
(ns myproject.urls
  (:require
   [compojure.core :refer [defroutes GET]]
   [compojure.route :as route]
   ;; 1) Import View
   [myproject.views.public :as view-public]))

;; 2) Set group routes, in the example it is called "public"
(defroutes public
  ;; 3) Add routes
  (GET "/" [] view-public/index)
  (GET "/FAQ" [] view-public/faq)
  (GET "/about" [] view-public/about))


(defroutes resources-routes
  ;; Resources (statics)
  (route/resources "/")
  (route/not-found view-public/page-404))

;; 4) Add your group of routes to all of them.
(def all-routes
  ;; Wrap routers. "resources-routes" should always be the last.
  (compojure.core/routes public resources-routes))

Parameters

In the following example we have routes that require different parameters.

(defroutes public
  (GET "/" [] view-public/index)
  (GET "/blog/:id" [id] view-public/blog)
  (GET "/auth/activate-account/:token/:email/" [token email] view-auth/activate-account))

In the View, the variables are collected as follows.

(defn activate-account
  "Activate account"
  [req]
  (let [token (-> req :params :token)
        email (-> req :params :email)]
    ;; Your magic code
    (redirect req "/auth/login/")))

Avoiding repetition

At some point you will need to have a prefix for the routes.

(defroutes user-routes
    (GET "/user/auth/login" [] ...)
    (GET "/user/auth/signup" [] ...)
    (GET "/user/auth/recovery-password" [] ...))

To avoid this you can use a context.

(defroutes user-routes
  (context "/user/auth" []
    (GET "/login" [] ...)
    (GET "/signup" [] ...)
    (GET "/recovery-password" [] ...)))

Views

Views are used to include the logic of each route.

This example renderes an HTML template.

(defn index
  ;; View HTML
  [req]
    (render-HTML req "public/welcome.html" {}))

The following example prints simple JSON.

(defn api
  ;; View JSON
  [req]
    (render-JSON req {:result true}))

;; { "result": true }

You can see more in Responses.

Templates

HTML templates can be rendered raw, using parameters or in different layouts. It is also possible to return Markdown or JSON.

All templates should be in /resources/templates/.

The syntax is inspired by Django. It is created by Selmer, you can refer to its documentation on more advanced topics like loops or filters.

HTML

Let's say you have an HTML template at /resources/templates/theater.html.

<!DOCTYPE html>
<html>
    <body>
        Olympia Theatre
    </body>
</html>

In this case, you just need to use the render-HTML function.

(render-HTML [request] [path] [args]))

Example:

;;;; View web
(ns myproject.views.my-view
  (:require
    [tadam.templates :refer [render-HTML]]))

(defn index
  ;; View HTML
  [req]
  (render-HTML req "theatre.html" {}))

HTML with params

Now, suppose you have an HTML template at /resources/templates/theater.html.

<!DOCTYPE html>
<html>
    <body>
         <p>The {{ name }} Theatre was founded in {{ opened }} and currently has {{ surface }} seats.</p> 
    </body>
</html>

The View will be similar to the previous example, but with the indication of its parameters.

(ns myproject.views.my-view
  (:require
    [tadam.templates :refer [render-HTML]]))

(defn index
    ;; View HTML
    [req]
    (render-HTML req "theatre.html" {
    :name "Olympia"
    :opened 1915
    :surface 500
    }))

As a result, the request is returned.

<!DOCTYPE html>
<html>
    <body>
         <p>The Olympia Theatre was founded in 1915 and currently has 500 seats.</p> 
    </body>
</html>

HTML in layout

If you need to repeat the same HTML structure, you can extend the template or define which part will change on each HTML page in the layout.

Let's create a template at /resources/templates/layouts/base.html, which will be our reference for generating new ones. It will contain all the repeating parts like header, footer, navigation, etc. Then we define where blocks should be added, which will be different on every page.

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/main.css">
    <script src="/js/main.js"> </script>
    <title>{% block title %}{% endblock %} | Olympia Theatre</title>
</head>
<body>
    <header>
        <nav>
            <ul>
                <li><a href="/">Welcome</a></li>
                <li><a href="/programme">Programme</a></li>
            </ul>
        </nav>
    </header>
    <main>
        {% block content %}{% endblock %}
    </main>
    <footer>
        More information in our newsletter
    </footer>
</body>
</html>

Now it's time to define a page that will extend base.html as a new template /resources/templates/public/welcome.html.

{% extends "layouts/base.html" %}

{% block title %}
Welcome
{% endblock %}

{% block content %}
<h1 class="title-welcome">Welcome to Olympia Theatre</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Conclusum est enim contra Cyrenaicos satis acute, nihil ad Epicurum. Qui bonum omne in virtute ponit, is potest dicere perfici beatam vitam perfectione virtutis; Ecce aliud simile dissimile. </p>
{% endblock %}

The result:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/main.css">
    <script src="/js/main.js"> </script>
    <title>Welcome | Olympia Theatre</title>
</head>
<body>
    <header>
        <nav>
            <ul>
                <li><a href="/">Welcome</a></li>
                <li><a href="/programme">Programme</a></li>
            </ul>
        </nav>
    </header>
    <main>
      <h1 class="title-welcome">Welcome to Olympia Theatre</h1>
      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Conclusum est enim contra Cyrenaicos satis acute, nihil ad Epicurum. Qui bonum omne in virtute ponit, is potest dicere perfici beatam vitam perfectione virtutis; Ecce aliud simile dissimile. </p>
    </main>
    <footer>
        More information in our newsletter
    </footer>
</body>
</html>

Markdown

You can use Markdown files to convert to HTML.

(ns myproject.views.my-view
  (:require
    [tadam.templates :refer [render-markdown]]))

(render-markdown req "theatre.md" {})

JSON

A function to convert collections to JSON is also available.

(render-JSON [request] [collection]))

Example

(ns myproject.views.my-view
  (:require
    [tadam.templates :refer [render-JSON]]))


(render-JSON req {:name "Olympia" :surface 500 :opened 1915})

It returns neat JSON with the correct header.

{
  "name": "Olympia",
  "surface": 500,
  "opened": 1915
}

Render templates to string

In certain circumstances, it is necessary to render HTML templates to obtain a string, for example when we want to prepare content to send an email or save it to a file.

(render-template [path template] [collection])

Example

(ns myproject.views.my-view
  (:require
   [tadam.templates :refer [render-template]]))

(render-template "public/template.html" {:name "Olympia" :surface 500 :opened 1915})

Requests

Params

GET

From the following URL, we can capture the query variable.

curl https://mydomain.com/?query=Tadam
(-> req :params :query)
;; Tadam

POST

Capturing a parameter using POST is similar to GET.

curl --data "query=Tadam" https://mydomain.com/
(-> req :params :query)
;; Tadam

In case you need to know if you are receiving a POST method.

(ns myproject.views.myview
  (:require [tadam.utils :refer [is-post]]))

(is-post req)
;; true or false

JSON

A small utility can be used to get JSON parameters.

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"query": "Tadam","page": 1}' \
  https://mydomain.com/
(ns myproject.views.myview
  (:require [tadam.utils :refer [get-JSON]]))

(get-JSON req)
;; {:query "Tadam" :page 1} 

HEADERS

In case you need to capture the value of a header.

(ns myproject.views.myview
  (:require [tadam.utils :refer [get-header]]))

(get-header req "Content-Type")
;; "application/json"

Responses

When using the (render-HTML) function or any other template element in a view, there is no need to specify a request. Provided you need to customize it, you have an assistant.

(response [req] [body] [status] [content-type]))

At least, request and body must be specified.

(ns myproject.views.my-view
  (:require
    [tadam.responses :refer [response]]))

(defn index
  ;; View HTML
  [req]
  (response req "Hi Tadam"))

Back to the browser.

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8

Hi Tadam

Template can also be customized with status and content-type.

(ns myproject.views.my-view
  (:require
    [tadam.responses :refer [response]]))

(defn index
  ;; View HTML
  [req]
  (response req "<?xml version="1.0"?>
    <Name>
        Tadam
    </Name>" 201 "text/xml;charset=utf-8"))

Back to the browser.

HTTP/1.1 201 OK
Content-Type: text/xml;charset=utf-8

<?xml version="1.0"?> <Name> Tadam </Name>

Redirect

To make a redirect in your view, you have a redirect utility inside responses.

(redirect [req] [url] [status]))

Example:

(ns myproject.views.my-view
  (:require
    [tadam.responses :refer [redirect]]))

(defn index
  ;; View HTML
  [req]
  (redirect req "/contact/"))

Unless otherwise specified, status 303 will be used (see "Other").

If you need a different status like 301, you can customize the argument status.

(ns myproject.views.my-view
  (:require
    [tadam.responses :refer [redirect]]))

(defn index
  ;; View HTML
  [req]
  (redirect req "/blog/tadam-is-magic/" 301))

You can also use redirect-permanent if you want to get the 308 status directly, although this can be done in the same way as in the previous examples.

(redirect-permanent [req] [url]))

Example:

(ns myproject.views.my-view
  (:require
    [tadam.responses :refer [redirect-permanent]]))

(defn index
  ;; View HTML
  [req]
  (redirect-permanent req "/blog/tadam-is-magic/"))

Email

If you would like to send emails, then you first need to update the config variables in config.yaml.

Add the following:

smtp-from: "no-reply@domain.com"
smtp-host: "smtp.domain.com"
smtp-user: "user"
smtp-password: "password"
smtp-port: 587
smtp-tls: true

Now all you have to do is use send-email.

(send-email [config] [recipient email] [subject] [message HTML] [message plain])

Example:

(ns myproject.views.my-view
  (:require
   [myproject.config :refer [config]
   [tadam.responses :refer [response]]
   [tadam.email :refer [send-email]]))

(defn send-message
  ;; View Send email
  [req]
    ;; Send email
    (send-email config "client@email.com" "My subject" "<h1>Title</h1><p>Content</p>" "Title\nContent")

    ;; Response OK
    (response req "Sent!!!!"))

You can make it even easier by customizing HTML or plain text with render-template.

(ns myproject.views.my-view
  (:require
   [myproject.config :refer [config]
   [tadam.responses :refer [response]]
   [tadam.templates :refer [render-template]]
   [tadam.email :refer [send-email]]))

(defn send-message
  ;; View Send email
  [req]
    (let [params {:name "Houdini"
                  :born 1874}]
    ;; Send email
    (send-email config 
         "client@email.com" 
         "My subject" 
         (render-template "emails/contact.html" params)
         (render-template "emails/contact.txt" params))

    ;; Response OK
    (response req "Sent!!!!")))

Compile

Run the following command to create a jar file.

lein uberjar

After executing the command, two files should be created in target /. We will use the standalone version: {project name}-standalone.jar.

Service

Systemctl

Service creation on Linux is the same as for any Java application. You can see an example below.

Create a file in the following path: /etc/systemd/system/tadam-project.service.

Add the content.

[Unit]
Description=Tadam-project
After=network.target

[Service]
Type=simple
Restart=always
WorkingDirectory=/folder/jar/
ExecStart=java -jar tadam-project.jar

[Install]
WantedBy=multi-user.target 

Finally enable and start the service.

sudo systemctl enable tadam-project
sudo systemctl start tadam-project

Reverse Proxy

Nginx

Deploying with Nginx is pretty quick and easy. You can use it as a reverse proxy. Below you can see an example configuration that you might find useful.

server {

        server_name tadam.domain.com;

        access_log /var/log/myproject_access.log;
        error_log /var/log/myproject_error.log;

        location / {
            proxy_pass http://localhost:7404/;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect  off;
        }
}

Donation

Every drop of coffee is transformed, multiplied by 2, and turned into teaching material in Clojure to bring the ecosystem closer to new developers. Are you ready to help me expand the community?

Talks

Charlando de desarrollo con Andros Fenollosa

Spanish

Tadam framework web en Clojure - República Web

Spanish

PyConES20 - Introducción a la Programación Funcional con Python

Spanish

Success stories

Glosa

Comments for static sites. A clone of Disqus, but faster, Open Source and sexy.

Want to know if a site has been created on Tadam?

curl -Is localhost:7404 | grep 'X-Powered-By:' 

👇

X-Powered-By: Clojure/Tadam

Thanks

Valentina Rubane: Editing of texts in English.