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-appscaffold 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 devOpen 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.jsonMake a change
Open src/routes/index.route.ts. This is the whole file as the scaffold writes it:
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 billingNext
Now that you have an app running, learn how the filesystem maps to URLs in Routing & layouts.