Getting Started with Nectar

This guide walks you through installing Nectar, writing your first program, and building a complete application.


Prerequisites

Nectar’s compiler is written in Rust. You need:

Verify your Rust installation:

rustc --version
cargo --version

Installation

Build from Source

Clone the Nectar repository and build the compiler:

git clone https://github.com/HibiscusConsulting/nectar-lang.git
cd nectar-lang
cargo build --release

The compiled binary is at target/release/nectar. Add it to your PATH:

export PATH="$PWD/target/release:$PATH"

Verify the installation:

nectar --version

Your First Nectar Program

Create a file called hello.nectar:

component Hello(name: String) {
    render {
        <div>
            <h1>"Hello from Nectar!"</h1>
            <p>{name}</p>
        </div>
    }
}

This defines a Hello component that takes a name prop and renders it inside a <p> tag.

Compiling

Compile to WebAssembly Text Format (WAT):

nectar build hello.nectar

This produces hello.wat. To compile directly to binary WebAssembly:

nectar build hello.nectar --emit-wasm

This produces hello.wasm.

Running in the Browser

You need three files in the same directory to run your component:

  1. hello.wasm — the compiled WebAssembly module
  2. core.js — the Nectar runtime (copied from the repo)
  3. index.html — the entry point that loads and runs your component

Step 1: Copy the runtime

The Nectar runtime lives in the repository. Copy it to your working directory:

cp nectar-lang/runtime/modules/core.js .

(If you installed Nectar via package manager, the path will vary; check your installation directory.)

Step 2: Create index.html

<!DOCTYPE html>
<html>
<head><title>My Nectar App</title></head>
<body>
  <div id="app"></div>
  <script type="module">
    import { instantiate } from './core.js';
    instantiate('hello.wasm').then(inst => inst.exports.main());
  </script>
</body>
</html>

Step 3: Use the built-in dev server

nectar dev --src . --port 3000

Open http://localhost:3000 to see your component.

The dev server will find your index.html, which imports core.js, which loads hello.wasm. All three files must be in the directory you run nectar dev from.

Alternative: Manual static server

If you prefer a different static server (Python, Node, etc.), make sure all three files (hello.wasm, core.js, index.html) are in the served directory.


Adding State and Reactivity

Let’s build a counter. Create counter.nectar:

component Counter(initial: i32) {
    let mut count: i32 = initial;

    fn increment(&mut self) {
        self.count = self.count + 1;
    }

    fn decrement(&mut self) {
        self.count = self.count - 1;
    }

    render {
        <div>
            <h2>"Counter"</h2>
            <span>{self.count}</span>
            <button on:click={self.increment}>"+1"</button>
            <button on:click={self.decrement}>"-1"</button>
        </div>
    }
}

Key concepts:

Compile and run:

nectar build counter.nectar --emit-wasm

Handling Events

Nectar uses the on:event syntax for DOM events. The handler is any expression, typically a method reference:

component Form() {
    let mut value: String = "";

    fn handle_submit(&mut self) {
        // Process the form
        println(f"Submitted: {self.value}");
        self.value = "";
    }

    render {
        <div>
            <input
                value={self.value}
                placeholder="Enter text..."
            />
            <button on:click={self.handle_submit}>"Submit"</button>
        </div>
    }
}

Common events: click, submit, input, change, mouseover, mouseout, keydown, keyup.


Fetching Data from an API

Nectar has first-class support for HTTP communication via the fetch keyword:

struct Post {
    id: u32,
    title: String,
    body: String,
}

store PostService {
    signal posts: [Post] = [];
    signal loading: bool = false;

    async action fetch_posts(&mut self) {
        self.loading = true;

        let response = await fetch("https://jsonplaceholder.typicode.com/posts");

        if response.status == 200 {
            self.posts = response.json();
        }
        self.loading = false;
    }

    async action create_post(&mut self, title: String, body: String) {
        let response = await fetch("https://jsonplaceholder.typicode.com/posts", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: f"{{\"title\": \"{title}\", \"body\": \"{body}\"}}",
        });

        if response.status == 201 {
            let new_post: Post = response.json();
            self.posts.push(new_post);
        }
    }
}

component PostList() {
    render {
        <div>
            <h1>"Posts"</h1>
            {if PostService::get_loading() {
                <div>"Loading..."</div>
            }}
            <ul>
                {for post in PostService::get_posts() {
                    <li>
                        <h3>{post.title}</h3>
                        <p>{post.body}</p>
                    </li>
                }}
            </ul>
        </div>
    }
}

Key concepts:


Building a Complete Todo App

Let’s walk through todo.nectar, a complete todo application demonstrating structs, enums, ownership, and components.

Step 1: Define Data Types

struct Todo {
    id: u32,
    text: String,
    done: bool,
}

enum Filter {
    All,
    Active,
    Completed,
}

Todo is a simple struct holding each item’s data. Filter is an enum for the three filter states.

Step 2: Build the Component

component TodoApp() {
    let mut todos: [Todo] = [];
    let mut next_id: u32 = 0;
    let mut filter: Filter = Filter::All;

The component maintains a list of todos, a counter for generating unique IDs, and the current filter.

Step 3: Add Business Logic

    fn add_todo(&mut self, text: String) {
        let todo = Todo {
            id: self.next_id,
            text: text,
            done: false,
        };
        self.next_id = self.next_id + 1;
        // Ownership: todo is moved into the collection
        self.todos.push(todo);
    }

    fn toggle(&mut self, id: u32) {
        for todo in &mut self.todos {
            if todo.id == id {
                todo.done = !todo.done;
            }
        }
    }

Notice the ownership semantics: todo is moved into the todos array – there is no implicit copy. The toggle method borrows the array mutably with &mut self.todos to modify items in place.

Step 4: Filter with Pattern Matching

    fn visible_todos(&self) -> [&Todo] {
        match self.filter {
            Filter::All => &self.todos,
            Filter::Active => self.todos.iter().filter(fn(t: &Todo) -> bool { !t.done }),
            Filter::Completed => self.todos.iter().filter(fn(t: &Todo) -> bool { t.done }),
        }
    }

Pattern matching on self.filter returns the appropriate subset. The return type [&Todo] signals that we return borrowed references, not copies.

Step 5: Render the UI

    render {
        <div>
            <h1>"Nectar Todo"</h1>
            <div>
                <input placeholder="What needs to be done?" />
                <button on:click={self.add_todo}>"Add"</button>
            </div>
            <ul>
                {self.visible_todos()}
            </ul>
        </div>
    }
}

The template renders the filtered todo list. Thanks to fine-grained reactivity, only the changed DOM nodes update when a todo is added or toggled.

Compile and Run

nectar build todo.nectar --emit-wasm
nectar dev --port 3000

Initializing a Project

For larger projects, use nectar init to create a project with dependency management:

mkdir my-app && cd my-app
nectar init --name my-app

This creates an Nectar.toml manifest:

[package]
name = "my-app"
version = "0.1.0"

[dependencies]

Add dependencies:

nectar add ui-components --version "^1.0"
nectar install

Build the project:

nectar build src/main.nectar --emit-wasm -O2

Component Composition with Routers

Nectar supports nested routing with layout components using <Outlet />:

component NavBar() {
    render {
        <nav>
            <a on:click={self.go_home}>"Home"</a>
            <a on:click={self.go_about}>"About"</a>
        </nav>
    }

    fn go_home(&mut self) {
        navigate("/");
    }

    fn go_about(&mut self) {
        navigate("/about");
    }
}

router AppRouter {
    layout {
        <div>
            <NavBar />
            <Outlet />
        </div>
    }

    route "/" => Home,
    route "/about" => About,
    fallback => NotFound,
}

The <Outlet /> element marks where the routed page content renders. The surrounding layout (NavBar) persists across navigations without re-rendering.


Generics and Monomorphization

Nectar supports generic functions that are specialized at compile time:

fn max<T>(a: T, b: T) -> T where T: Ord {
    if a > b { a } else { b }
}

let bigger = max(10, 20);          // generates max__i32
let longer = max("abc", "xyz");    // generates max__String

The compiler creates a separate WASM function for each concrete type, eliminating runtime type dispatch.


Next Steps

Now that you have the basics, explore these resources: