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 πŸŽ‰. You already have your πŸ”₯ own web server πŸ”₯.

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 you will have a config.yaml file like the one below.

domain: "http://localhost"
debug: true
port: 7404
  • domain: Indicates which will be the domain of your application (http://example.com). In case you work locally or have a reverse proxy, it will not be necessary to modify it.
  • debug: If true, it enables an automatic code refresh and ignores CORS. Otherwise it assumes you are working in production and will block the CORS in accordance to the domain.
  • port: Port you will use.

Feel free to add as many variables as you need.

For example let's create the 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 with View.
  4. Add your group to the set of all routes (Optional, only if it does not exist). You should always leave at the end resources-routes, are used for static content.
(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

The views are used to include the logic of each route.

In this example, an HTML template is rendered.

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

In the following example, a simple JSON is printed.

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

;; { "result": true }

You can see more in Responses.

Templates

HTML templates can be displayed in raw, rendered using parameters or different layouts. As well as it is possible to return Markdown or JSON.

All templates should be located in /resources/templates/.

The syntax is inspired by Django. It is generated by Selmer, you can consult its documentation for more advanced topics such as loops or filters.

HTML

Suppose you have an HTML template in the /resources/templates/theatre.html path.

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

Then you just need to use the function render-HTML.

(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

Suppose you have an HTML template in the /resources/templates/theatre.html path.

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

The View would be similar to the previous example but indicating 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

In case you need to repeat the same HTML structure, then it is possible to extend a template or to define which part will change at each page HTML in layout.

Let's create a template in /resources/templates/layouts/base.html that will our reference to generate new ones. It will contain all the recurrent parts, such as header, footer, navs, etc. Then we define where should be added blocks that will be different at each 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 the page that will extend base.html with new template in /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 is:

<!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

It is possible to use Markdown files that will be transformed into HTML.

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

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

JSON

There is a function available to convert collections to JSON.

(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 a nice JSON with the right 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 because it is going to be saved in a new 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 variable query.

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

POST

Capturing a parameter by POST is like GET.

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

If you need to know if you are receiving the POST method you can use:

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

(is-post req)
;; true or false

JSON

A small utility can be used to obtain the parameters of a JSON.

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} 

Responses

When used in a View the (render-HTML) function, or any other template element, it is not necessary to specify the request. In case you need to customize it you have a helper.

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

At least the request and the body must be indicated.

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

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

Return to browser.

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

Hi Tadam

Although it can 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"))

Return to 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 within responses the utility redirect.

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

Example:

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

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

If he's not told otherwise, he'll use status 303 (See Other).

If you need another one, such as 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))

Also redirect-permanent in case you want to use the 308 state directly, although it can also be done like 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 want to send emails before you must update the configuration 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

Then all you have to do is use send.

(send [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]]))

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

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

You can do this easily by customizing the 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]]))

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

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

Compile

Run the following command to build a jar file.

lein uberjar

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

Service

Systemctl

To create a service in Linux is done like any application in Java. Below you can see an example.

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

With Nginx it's pretty quick and easy. You can use it as a reverse proxy, since Glosa contains its own web server (Jetty). You can see an example of configuration that can be 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;
        }
}

Success stories

Glosa

Comments for static sites. Clone of Disqus, but faster, Opensource and sexy.

Is it wordpress

Free online service that tells you if a site is made with WordPress using proprietary techniques.

Do you want to know if a site is made with Tadam?

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

πŸ‘‡

X-Powered-By: Clojure/Tadam

Thanks

Valentina Rubane: Revision of texts in English.