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;
}
}
🧩 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>
</|>
📦 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 }
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 }
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
}
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;
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 }
🔁 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)