FDD Story Get Started Tutorial Docs Motivation Download GitHub

Build StatusPost

A step-by-step guide to building a real-time message board with ARO. WebSockets, repositories, templates, and more.

11 steps ~20 minutes Intermediate
Step 1

The Story

What we're building and why

Imagine you want to build a simple real-time message board. Users can post short status updates, and everyone connected sees new messages instantly. No page refresh. No polling. Just real-time updates via WebSocket.

StatusPost is exactly that. It's a single-page app where you type a message, hit Post, and it appears for everyone. The server stores messages in a repository, broadcasts them via WebSocket, and even cleans up old messages automatically.

By the end of this tutorial, you'll understand:

Contract-First APIs

Define your HTTP routes in OpenAPI before writing code

WebSocket Support

Enable real-time bidirectional communication

Repositories

Store and retrieve data with simple actions

Repository Observers

React automatically when data changes

Templates

Render dynamic HTML with ARO expressions

Let's build it, step by step.

Step 2

Project Structure

Setting up the files

An ARO application is a directory containing .aro files and an optional openapi.yaml for HTTP routing. Here's what StatusPost looks like:

StatusPost/
  openapi.yaml      # HTTP routes (contract-first)
  main.aro          # Application lifecycle
  api.aro           # HTTP handlers
  websocket.aro     # WebSocket handlers
  templates/
    index.html      # Main page template
    static/
      style.css     # Styles
      app.js        # Client-side JavaScript

Create this directory structure. We'll fill in each file as we go.

ARO automatically discovers all .aro files in your application directory. No imports needed. Feature sets are globally visible.

Step 3

The Contract

Define the API first

ARO uses contract-first development. You define your HTTP routes in an OpenAPI specification, and ARO routes requests to feature sets based on the operationId.

StatusPost needs four endpoints: serve the homepage, serve static files, get messages, and post a new message.

openapi.yaml
openapi: 3.0.3
info:
  title: StatusPost API
  version: 1.0.0

paths:
  /:
    get:
      operationId: homePage
      summary: Serve the main HTML page

  /static/{file}:
    get:
      operationId: serveStatic
      summary: Serve static files (CSS, JS)
      parameters:
        - name: file
          in: path
          required: true
          schema:
            type: string

  /messages:
    get:
      operationId: getMessages
      summary: Get last 20 messages

    post:
      operationId: postMessage
      summary: Post a new message
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                message:
                  type: string

components:
  schemas:
    Message:
      type: object
      properties:
        id:
          type: string
        message:
          type: string
        createdAt:
          type: string
          format: date-time

Each operationId maps directly to a feature set name. When a request hits GET /, ARO looks for a feature set named homePage.

Step 4

Application Startup

Where everything begins

Every ARO application needs exactly one Application-Start feature set. This is where you initialize services and prepare for events.

StatusPost needs an HTTP server with WebSocket support. We enable WebSocket by passing a configuration object to the Start action.

main.aro
(* StatusPost - Real-time message board with WebSocket updates *)

(Application-Start: StatusPost) {
    Log "Starting StatusPost..." to the <console>.

    (* Start HTTP server with WebSocket on /ws path *)
    Start the <http-server> with { websocket: "/ws" }.

    Log "StatusPost ready on http://localhost:8080" to the <console>.
    Log "WebSocket available on ws://localhost:8080/ws" to the <console>.

    (* Keep the application running to handle events *)
    Keepalive the <application> for the <events>.

    Return an <OK: status> for the <startup>.
}

(Application-End: Success) {
    Log "StatusPost shutting down..." to the <console>.
    Stop the <http-server> with {}.
    Return an <OK: status> for the <shutdown>.
}

The Keepalive action is essential for long-running applications. Without it, the application would exit after startup completes.

Step 5

Serving Pages

HTML templates and static files

Now we need to handle HTTP requests. The homePage feature set serves the main HTML page. It retrieves recent messages from the repository and transforms them through a template.

api.aro (part 1)
(* Serve static files - CSS, JS *)

(serveStatic: StatusPost API) {
    Extract the <file> from the <pathParameters: file>.
    Create the <template-path> with "static/" ++ <file>.
    Transform the <content> from the <template: template-path>.
    Return an <OK: status> with <content>.
}

(* Serve the homepage with recent messages *)

(homePage: StatusPost API) {
    Retrieve the <all-messages> from the <message-repository>.
    Extract the <recent-messages: 0-19> from the <all-messages>.
    Transform the <html> from the <template: index.html>.
    Return an <OK: status> with <html>.
}

(* Get messages as JSON for the API *)

(getMessages: StatusPost API) {
    Retrieve the <all-messages> from the <message-repository>.
    Extract the <recent-messages: 0-19> from the <all-messages>.
    Return an <OK: status> with <recent-messages>.
}

Notice the Extract with 0-19? That's a range specifier. It extracts the first 20 elements from the list. ARO supports first, last, index access, and ranges.

The Transform action processes a template file. Variables from the current scope (like recent-messages) are available inside the template.

Step 6

Posting Messages

Store and broadcast

When a user posts a message, we need to create a message object, store it in the repository, and broadcast it to all connected WebSocket clients.

api.aro (part 2)
(* Post a new message *)

(postMessage: StatusPost API) {
    (* Extract the message text from the form data *)
    Extract the <body> from the <request: body>.
    Extract the <message-text: message> from the <body>.

    (* Create a message object with timestamp *)
    Create the <message: Message> with {
        message: <message-text>,
        createdAt: <now>
    }.

    (* Store in repository *)
    Store the <message> into the <message-repository>.

    (* Broadcast to all WebSocket clients *)
    Broadcast the <message> to the <websocket>.

    Return a <Created: status> with <message>.
}

The Store action saves the message to an in-memory repository. Repositories are named collections that persist for the lifetime of the application.

The Broadcast action sends the message to all connected WebSocket clients. They'll receive it as JSON and update their UI in real-time.

The <now> special variable gives you the current timestamp. ARO automatically serializes it to ISO 8601 format for JSON responses.

Step 7

Real-Time Updates

WebSocket connection events

When clients connect and disconnect via WebSocket, ARO fires events that you can handle with feature sets. The business activity pattern is WebSocket Event Handler.

websocket.aro
(* WebSocket Event Handlers *)

(Handle WebSocket Connect: WebSocket Event Handler) {
    Extract the <connection-id> from the <event: id>.
    Log "WebSocket client connected" to the <console>.
    Return an <OK: status> for the <connection>.
}

(Handle WebSocket Disconnect: WebSocket Event Handler) {
    Extract the <connection-id> from the <event: connectionId>.
    Log "WebSocket client disconnected" to the <console>.
    Return an <OK: status> for the <disconnection>.
}

These handlers are optional but useful for logging and tracking connected clients. The magic happens in Broadcast which ARO handles internally.

Step 8

Cleanup Observer

React when data changes

What happens when hundreds of messages pile up? We need automatic cleanup. Repository observers let you react when data is stored, updated, or deleted.

We'll create an observer that triggers when the message count exceeds 40. It keeps the 20 most recent messages and clears the rest.

api.aro (part 3)
(* Repository Observer - cleanup when message count exceeds 40 *)

(Cleanup Messages: message-repository Observer) when <message-repository: count> > 40 {
    Retrieve the <all-messages> from the <message-repository>.
    Extract the <keep-messages: 0-19> from the <all-messages>.
    Clear the <all> from the <message-repository>.
    Store the <keep-messages> into the <message-repository>.
    Log "Cleaned up messages, kept last 20" to the <console>.
    Return an <OK: status> for the <cleanup>.
}

The when clause is a guard condition. The observer only fires when the condition is true. <message-repository: count> returns the current number of items in the repository.

Without the when guard, the observer would fire on every store operation. The condition prevents unnecessary cleanup when there are few messages.

Step 9

Templates

Dynamic HTML rendering

ARO templates are HTML files with embedded expressions. Use {{ }} for values and {{ for each }} for loops.

templates/index.html
<!DOCTYPE html>
<html>
<head>
    <title>StatusPost</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <h1>StatusPost</h1>

    <div class="form">
        <form id="f">
            <input type="text" id="msg" placeholder="Message..." required>
            <button type="submit">Post</button>
        </form>
    </div>

    <div id="status" class="disconnected">Connecting...</div>

    <div id="messages">
{{ for each <msg> in <recent-messages> { }}
        <div class="message">
            <div class="time">{{ <msg: createdAt> | date: "dd.MM.yyyy HH:mm" }}</div>
            <div>{{ <msg: message> }}</div>
        </div>
{{ } }}
    </div>

    <script src="/static/app.js"></script>
</body>
</html>

The template loops through recent-messages and renders each one. The | date: filter formats the timestamp.

The client-side JavaScript connects to WebSocket and updates the page in real-time:

templates/static/app.js
var m = document.getElementById('messages');
var s = document.getElementById('status');
var f = document.getElementById('f');
var i = document.getElementById('msg');

function add(msg) {
    var d = document.createElement('div');
    d.className = 'message';
    d.innerHTML = '<div class="time">' + formatDate(msg.createdAt) + '</div>' +
                  '<div>' + escapeHtml(msg.message) + '</div>';
    m.insertBefore(d, m.firstChild);
}

var ws;
function connect() {
    var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
    ws = new WebSocket(protocol + '//' + location.host + '/ws');

    ws.onopen = function() {
        s.textContent = 'Connected';
        s.className = 'connected';
    };

    ws.onmessage = function(e) {
        try { add(JSON.parse(e.data)); } catch(err) {}
    };

    ws.onclose = function() {
        s.textContent = 'Disconnected';
        s.className = 'disconnected';
        setTimeout(connect, 3000);
    };
}
connect();

f.onsubmit = function(e) {
    e.preventDefault();
    var msg = i.value.trim();
    if (!msg) return;
    fetch('/messages', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: 'message=' + encodeURIComponent(msg)
    });
    i.value = '';
};
Step 10

Running It

See it in action

With all files in place, run your application:

Terminal
$ aro run ./StatusPost

Starting StatusPost...
StatusPost ready on http://localhost:8080
WebSocket available on ws://localhost:8080/ws

Open http://localhost:8080 in your browser. Open another browser window. Post a message. Watch it appear instantly in both windows.

Post 41 messages. Watch the console log the cleanup. The observer keeps your repository lean automatically.

That's StatusPost. A real-time message board with HTTP, WebSocket, repositories, observers, and templates. All in about 50 lines of ARO code.

The complete source code is available on GitHub: arolang/example-web-chat

Step 11

Build It

Compile to a native binary

Running with aro run is great for development, but for production you want a native binary. ARO compiles to LLVM IR and links against the runtime to produce a standalone executable.

Terminal
$ aro build ./StatusPost

Compiling StatusPost...
  Generating LLVM IR...
  Compiling to object file...
  Linking with runtime...

Build complete: ./StatusPost/StatusPost

$ ./StatusPost/StatusPost

Starting StatusPost...
StatusPost ready on http://localhost:8080
WebSocket available on ws://localhost:8080/ws

The compiled binary includes everything: your ARO code, the runtime, and all dependencies. No interpreter needed. Just copy and run.

Use aro build --optimize for release builds. This enables LLVM optimizations for smaller binaries and faster execution.

Keep Building

You've built a real-time application. Now explore more of what ARO can do.