DEV Community

Fernando Correa de Oliveira
Fernando Correa de Oliveira

Posted on

3

Building Raku Codeboard: A Simple Full-Stack App with Red, Cromponent, and HTMX

Welcome to a walkthrough of Raku Codeboard — a small but complete full-stack web application built entirely in Raku. It lets you submit, store, and discuss code or comments under different topics. This blog post explains how it works using your actual code and shows how it leverages Red, Cromponent, and HTMX together.

👉 Source Code: https://github.com/FCO/Discuss


💡 What is Raku Codeboard?

Raku Codeboard is a lightweight app where users can:

  • Create topics
  • Post messages under each topic (text or code)
  • See a list of all people who participated

The stack:

  • Red for ORM/database mapping
  • Cromponent for reusable web components
  • HTMX for declarative interactivity

🧱 app.raku

This is your full app.raku, setting up Cro and wiring routes to Cromponents:

#!/usr/bin/env raku

use lib "lib";
use lib "bin/lib";
use Cro::HTTP::Router;
use Cro::HTTP::Server;
use Cro::WebApp::Template;
use Red:api<2>;

use Topic;
use Topics;
use People;
use Person;
use Message;

my $routes = route {
    red-defaults "SQLite";
    my $schema = schema(Topic, Person, Message).create;

    Topic.^add-cromponent-routes;
    Topics.^add-cromponent-routes;
    People.^add-cromponent-routes;

    template-location "resources/";
    get  -> {
        template "index.crotmp", %(
            topics => Topic.^all,
            people => Person.^all,
        )
    }
}

my Cro::Service $http = Cro::HTTP::Server.new(
    http => <1.1>,
    host => "0.0.0.0",
    port => 3013,
    application => $routes,
);

$http.start;
say "Listening at http://0.0.0.0:3013";
react {
    whenever signal(SIGINT) {
        say "Shutting down...";
        $http.stop;
        done;
    }
}
Enter fullscreen mode Exit fullscreen mode

🧩 index.crotmp

Your template renders two Cromponents: <&People> and <&Topics>:

<:use Boilerplate>

<|Boilerplate(:title('Todo - test cromponent'), :htmx)>
    <style>
         div.topics:empty:before {
            content: "No topics yet. Be the first to start one!";
        }
         div.messages:empty:before {
            content: "No messages yet. Be the first to create one!";
        }
         div.people:empty:before {
            content: "No people yet. Be the first one!";
        }
    </style>
    <a
        href="/topics"
        hx-get="/topics"
        hx-target=".main"
    >
        Topics
    </a>

    <a
        href="/people"
        hx-get="/people"
        hx-target=".main"
    >
        People
    </a>

    <h1>Raku Codeboard</h1>

    <div class="main" hx-trigger="load" hx-get="/topics"></div>
</|>

Enter fullscreen mode Exit fullscreen mode

📦 Cromponents and Models

Topics.rakumod

use Cromponent;
use Topic;

unit class Topics does Cromponent;

has @.topics = Topic.^all;

method LOAD { ::?CLASS.new }

method RENDER {
    Q:to/HTML/;
    <h2>Create a new topic</h2>
    <form
        method="post"
        action="/topic"
        hx-post="topic"
        hx-target=".topics"
        hx-swap="beforeend"
        hx-on::after-request="this.reset()"
    >
        <input type="text" name="nick" placeholder="Your nick" required>
        <input type="text" name="title" placeholder="Topic title" required>
        <button>Start Topic</button>
    </form>

    <h2>Topics</h2>
    <div class="topics"><@.topics.Seq><&HTML(.Card)></@></div>
    HTML
}

sub EXPORT { Topics.^exports }
Enter fullscreen mode Exit fullscreen mode

Topic.rakumod

use Red:api<2>;
use Cromponent;
use Person;
use Topic::Card;

unit model Topic does Cromponent is table<topic>;

has UInt $.id        is serial;
has Str  $.title     is unique{ :nullable };
has      @.messages  is relationship(*.topic-id, :model<Message>);
has UInt $.author-id is referencing(*.id, :model<Person>);
has UInt $.author    is relationship(*.author-id, :model<Person>);

method LOAD(UInt() $id) { ::?CLASS.^load: $id }
method DELETE           { $.^delete }
method CREATE(Str :$nick, Str :$title) {
    Topic.^create(:$title, :author{ :$nick }).Card
}

method message(Str :$body!, Str :$nick!, Bool :$code = False) is accessible{ :http-method<POST> } {
    $.messages.create: :$body, :$code, :author{ :$nick }
}

method RENDER {
    Q:to/HTML/;
    <div class="topic">
        <strong><.title></strong> <small>by <.author.nick></small>
        <div class=messages><@.messages.Seq><&HTML($_)></@></div>
        <form
            method="POST"
            action="/topic/<.id>/message"
            hx-post="/topic/<.id>/message"
            hx-target=".main"
            hx-on::after-request="this.reset()"
        >
            User name: <input name="nick"><br>
            <input type="checkbox" name="code"> code<br>
            <textarea name="body"></textarea><br>
            <button>Send</button>
        </form>
    </div>
    HTML
}

method Card {
    Topic::Card.new: :$!id, :$!title, :$!author
}

sub EXPORT { Topic.^exports }
Enter fullscreen mode Exit fullscreen mode

Topic::Card.rakumod

use Red;
use Cromponent;

unit model Topic::Card does Cromponent;

has UInt $.id     is required;
has Str  $.title  is required;
has      $.author is required;

method RENDER {
    Q:to/HTML/
    <div class="topic">
        <a
            href="/topic/<.id>"
            hx-get="/topic/<.id>"
            hx-target=".main"
        >
            <strong><.title></strong>
        </a>
        <small>by <.author.nick></small>
    </div>
    HTML
}
Enter fullscreen mode Exit fullscreen mode

Message.rakumod

use Red;
use Cromponent;

unit model Message does Cromponent is table<message>;

has UInt $.id;
has UInt $.topic-id is referencing(*.id, :model<Topic>);
has $.topic is relationship(*.topic-id, :model<Topic>);
has Str $.body;
has UInt $.author-id is referencing(*.id, :model<Person>);
has $.author is relationship(*.author-id, :model<Person>);
has Bool $.code;

method LOAD(Str() $id) { ::?CLASS.^load: $id }
method DELETE          { $.^delete }

method RENDER {
    Q:to/HTML/
    <div class="message">
        <.author.nick><br>
        <.body>
    </dev>
    HTML
}

subset Text is sub-model of Message where *.code.not;
subset Code is sub-model of Message where *.code.so;
Enter fullscreen mode Exit fullscreen mode

People.rakumod

use Cromponent;
use Person;

unit class People does Cromponent;

has @.people = Person.^all;

method LOAD { ::?CLASS.new }

method RENDER {
    Q:to/HTML/;
    <h2>People</h2>
    <div class="people"><@.people.Seq><div><&HTML($_)></div></@></div>
    HTML
}

sub EXPORT { People.^exports }
Enter fullscreen mode Exit fullscreen mode

🔁 HTMX + Cromponent + Red

Everything works together like this:

  • Forms use hx-post to Cromponent route methods (e.g., CREATE, message)
  • Cromponent methods use Red to insert into the database
  • They return a Cromponent that gets rendered to HTML
  • HTMX places that HTML back into the DOM (e.g., .main or .topics)

All routes are declared using .^add-cromponent-routes, so you don’t need to write routing logic by hand.


✅ Summary

This app is fully functional and real — and it’s all in Raku. It combines:

  • Red for database modeling
  • Cromponent for reusable UI
  • HTMX for user interactivity
  • Cro for the HTTP layer

🔗 https://github.com/FCO/Discuss

Let me know what you’d like to add!

Top comments (0)