palinxdocs

Your first app

This page takes you from nothing to a running palinx app with live reload.

Note: two ways in. Learn from scratch builds a small app by hand, one palinx concept per page, starting from a plain Bun.serve. This page scaffolds a complete app in one command and tours it.

Scaffold

Install the px CLI once, then use scaffold to scaffold a new project from the webapp template:

bun add -g @palinx/cli
px scaffold webapp my-app
cd my-app

scaffold writes the project, installs dependencies, and leaves you with a complete full-stack app: file-based routes, a layout, a couple of components, services, and API endpoints. The webapp template is one of several project shapes; Scaffolding covers the others and when to reach for them.

Run

Start the dev server:

bun run dev

Open http://localhost:2500. You'll see the starter app. The dev server watches src/ and reloads on change.

The dev script is px dev. With the default plain-CSS styling there's nothing else to run; a project scaffolded with --styles sass or --styles tailwind also starts the stylesheet watcher. Styling has the details.

The shape of a project

This is every file px scaffold webapp writes, generated from the template itself. Installing dependencies adds node_modules/ beside them.

my-app/
├── src/
│   ├── api/                         # JSON endpoints under /api
│   │   ├── tasks.ts
│   │   └── user.ts
│   ├── components/                  # reusable view pieces
│   │   ├── icons.ts
│   │   ├── navbar.ts
│   │   └── task-card.ts
│   ├── lib/                         # small client helpers
│   │   └── csrf-fetch.ts
│   ├── routes/                      # pages; the filesystem is the router
│   │   ├── tasks/
│   │   │   ├── _guard.ts            # protects everything under /tasks
│   │   │   └── index.route.ts
│   │   ├── _layout.ts               # wraps every page
│   │   ├── [...catchall].route.ts
│   │   ├── about.route.ts
│   │   ├── index.route.ts
│   │   ├── login.route.ts
│   │   └── register.route.ts
│   ├── services/                    # injectable classes (data, auth, ...)
│   │   ├── auth-api.service.ts
│   │   ├── auth.service.ts
│   │   └── db.service.ts
│   └── styles/
│       └── global.css               # the app's styles, linked on every page
├── tests/                           # bun test suites, run with px test
│   ├── components.test.ts
│   └── sessions-migration.test.ts
├── .env.example                     # environment variables the app reads
├── .gitignore
├── bun.lock
├── bunfig.toml
├── package.json
├── palinx.config.ts                 # server + router configuration
├── README.md
└── tsconfig.json

Make a change

Open src/routes/index.route.ts. This is the whole file as the scaffold writes it:

src/routes/index.route.ts
import { Route, html } from "@palinx/core";
import { currentUser } from "../components/navbar";
import { AuthIcon, DataIcon, ReactiveIcon } from "../components/icons";

export default class Index extends Route {
  private feature(icon: Node, title: string, desc: string) {
    return html`
      <div class="card">
        <div class="card-icon">${icon}</div>
        <h3 class="card-title">${title}</h3>
        <p class="card-text">${desc}</p>
      </div>
    `;
  }

  private landingContent() {
    return html`
      <div class="landing">
        <div class="hero">
          <p class="eyebrow">Full-stack, one runtime</p>
          <h1 class="hero-title">A tasks app, built end to end.</h1>
          <p class="lead">
            Signals, file-based routing, server rendering, class-based APIs, SQLite, and cookie auth,
            all in one Bun-native project. Sign up and start adding tasks.
          </p>
          <div class="actions">
            <a href="/register" class="btn btn-primary">Get started</a>
            <a href="/login" class="btn btn-secondary">Sign in</a>
          </div>
        </div>

        <div class="features">
          ${this.feature(AuthIcon(), "Cookie auth", "Register, login, and sessions backed by an @api service and SQLite.")}
          ${this.feature(DataIcon(), "Persisted data", "Tasks are stored in SQLite through an injectable data service.")}
          ${this.feature(ReactiveIcon(), "Reactive UI", "Signals keep the task list and counters in sync with no manual DOM work.")}
        </div>
      </div>
    `;
  }

  private authenticatedRedirect() {
    window.location.href = "/tasks";
    return html`<p class="notice">Redirecting...</p>`;
  }

  render() {
    return html`
      <div>
        ${currentUser() ? this.authenticatedRedirect() : this.landingContent()}
      </div>
    `;
  }
}

A route is a class that extends Route and returns markup from render(), the method at the bottom. The helper methods above it build the two states of the home page: the landing content for visitors, and a redirect to /tasks once you are signed in.

Change the text inside the <h1> in landingContent(), save, and the browser updates without a full reload.

Add a page

Create src/routes/hello.route.ts:

import { Route, html } from "@palinx/core";

export default class Hello extends Route {
  render() {
    return html`<h1>Hello</h1>`;
  }
}

Visit http://localhost:2500/hello. The file's location determined its URL, with no route table to register.

Scaffold individual files

Once a project exists, make generates the boilerplate for a new component, service, or route:

px make route dashboard
px make component user-badge
px make service billing

Next

Now that you have an app running, learn how the filesystem maps to URLs in Routing & layouts.