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.
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.
- Import the View.
- Use or create a group of Routes.
- Define the Routes using View.
- 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/"))
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
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.