Putting It All Together — Clean Language
← All tutorials
web-app10 min

Putting It All Together

Let's build a bookmarks manager — add URLs with a title and see them listed. This tutorial ties together everything from this track: server setup, database reads and writes, forms, components, and listing records. Small enough to understand completely, real enough to be useful.

The server file declares plugins, imports helpers, and lists all routes:

plugins:\n    frame.server\n    frame.data\n\nimport:\n    "helpers.cln"\n\nendpoints server:\n    GET "/" :\n        return http.respond(200, "text/html", render_home())\n\n    GET "/add" :\n        return http.respond(200, "text/html", render_add_form())\n\n    POST "/add" :\n        string title = req.body("title")\n        string url = req.body("url")\n        if title == "" or url == ""\n            return http.respond(400, "text/html", render_add_form())\n        string params = "[\\"" + title + "\\", \\"" + url + "\\"]"\n        db.query("INSERT INTO bookmarks (title, url) VALUES (?, ?)", params)\n        return http.respond(200, "text/html", render_home())
GET  /     → bookmarks list page\nGET  /add  → add bookmark form\nPOST /add  → save and show updated list

The server file is thin — routes only. Three routes cover the full cycle: list, show form, save. Validate before inserting and return the form with 400 on error. Delegate rendering to helpers.cln to keep the server file readable.

The render_home() function in helpers.cln fetches and displays all bookmarks:

functions:\n    string bookmark_item(string title, string url)\n        html:\n            <li class="bookmark-item">\n                <a href="{url}" target="_blank">{title}</a>\n            </li>\n\n    string render_home()\n        string cnt_res = db.query("SELECT CAST(COUNT(*) AS CHAR) as cnt FROM bookmarks", "[]")\n        integer count = json.get(cnt_res, "data.rows.0.cnt").toInteger()\n        string bm_res = db.query("SELECT title, url FROM bookmarks ORDER BY id DESC", "[]")\n        string items = ""\n        iterate i in 0 to count - 1\n            string t = json.get(bm_res, "data.rows." + i.toString() + ".title")\n            string u = json.get(bm_res, "data.rows." + i.toString() + ".url")\n            items = items + bookmark_item(t, u)\n        html:\n            <!DOCTYPE html><html><body>\n            <h1>My Bookmarks</h1>\n            <a href="/add">+ Add Bookmark</a>\n            <ul>{!items}</ul>\n            </body></html>
Renders a page with all saved bookmarks as clickable links

render_home() applies the listing pattern from the previous tutorial: count, fetch, iterate, render. The bookmark_item component is called once per row. Separation of concerns: server.cln owns routing, helpers.cln owns rendering.

Quick recap

  • Keep server.cln thin — routes only, delegate rendering to helpers.cln
  • Three routes cover a full feature: list, show form, save
  • Validate form input before inserting — return the form with 400 on error
  • The same patterns repeat in every app: query, count, iterate, render
Copied!