Points
- Category: Getting Started
A point is the one building block in Point0. A page is a point; so is a query, a
mutation, a layout, the root itself. They're all instances of a single Point0
class, composed with the same builder — you open a point, configure it, and
close it. The word points is the umbrella term: pages, layouts, queries,
mutations, and the rest are points.
export const ideaQuery = root.lets
.query() // open a query point
.input(z.object({ id: z.number() }))
.loader(async ({ input }) => ({ idea: await getIdea(input.id) }))
.query() // close it into a ready query
export const ideaPage = root.lets
.page('/ideas/:id') // open a page point
.with(ideaQuery, ({ params }) => ({ id: Number(params.id) }))
.page(({ data: { idea } }) => <h1>{idea.title}</h1>) // close itTwo points, two types, one grammar. The rest of this page lists every point type
and explains the two ways to write .lets.
The point types
There are eleven. They split into three groups by what they do.
Mountables — they render UI.
- page — a route and a component, with data, loading, error, and SSR handled up the chain.
- layout — a shared shell around pages, with its own route, data, loading, and error that pages inherit.
- component — composes queries and UI but has no route: a reusable mountable, not a page.
- provider — loads or computes a value once and hands it to every
child; read it with
.useValue()or.getValue().
Page, layout, component, and provider share one method-injection model — see mountable for what they have in common.
Data and endpoints — they move data, often over HTTP.
- query — an input schema plus a server loader: a real HTTP endpoint and a TanStack Query in one.
- infinite-query — a query that loads in pages (cursor or offset), with the full TanStack infinite cache.
- mutation — an input schema plus a loader: a real HTTP
POSTendpoint and a TanStack Mutation in one. - action — a server endpoint where you control the HTTP method and
path, and may return a raw
Response.
Structure — they hold defaults and shape the tree.
- root — the point you build everything from: the server entry point and the holder of defaults for every point beneath it.
- base — shared settings for a subset of points (a route prefix, defaults, gating) that its children inherit.
- plugin — a bundle of methods you define once and inject into any
point's chain with
.use().
A point is not always an HTTP endpoint. A query, mutation, or action always is (its own path, in the OpenAPI spec). A page or component is an endpoint only when it has a server loader (or SSR is on) — otherwise it's a pure mountable. Each point's own page says exactly when.
The short .lets notation
Every example in these docs opens a point with the short form —
root.lets.page('/ideas'), root.lets.query(), root.lets.mutation(). It
reads cleanly because the point's type is the method name and its name is
read from the variable:
export const ideaPage = root.lets.page('/ideas/:id').page(/* ... */)
// ▲ ▲
// name → 'idea' type → 'page'The variable is ideaPage; the compiler strips the type suffix and the point's
name becomes idea. Same rule everywhere — ideaQuery → idea, saveAction →
save, mainRoot → main. A variable with no matching suffix is kept as-is
(ideaX stays ideaX). If the variable name strips to nothing, or you use a
default export, the name falls back to the file path — the filename, or the
directory name for an index file.
The short notation needs the compiler. It's a compile-time rewrite, not a
runtime feature: without the compiler, root.lets.page(...) throws
lets[type] notation can not work without compiler, please use compilerSo short .lets.<type>() only works inside a Point0 app, where the compiler
runs.
The full .lets(type, name, …) form
The short form is sugar. The compiler rewrites every .lets.<type>(...) into
one explicit .lets(...) call — type first, name second, then whatever
arguments the short form took:
root.lets.page('/ideas') // → root.lets('page', 'idea', '/ideas')
root.lets.query() // → root.lets('query', 'idea')
root.lets.action('POST', '/save') // → root.lets('action', 'save', 'POST', '/save')The full form runs at runtime unchanged — no compiler required. Use it when you need to pin a name yourself (the variable name isn't a good source) or when the compiler isn't in play:
export const generalLayout = root.lets('layout', 'general').layout(/* ... */)Both forms are valid and identically typed. The examples in these docs use the
short form throughout for readability; a few places in the example apps use the
full form where a name is pinned explicitly (and the expo example, which
doesn't run generate, uses it everywhere).
Static vs instance .lets
Most points grow off another point's instance — root.lets.page(...),
generalLayout.lets.page(...), root.lets.query(...). Two types are different:
root and plugin are created from the Point0 class itself, with the
static Point0.lets:
import { Point0 } from '@point0/core'
export const root = Point0.lets.root().serverUrl(/* ... */).root()
export const mePlugin = Point0.lets.plugin().with(/* ... */).plugin()Point0.lets accepts only 'root' and 'plugin'; every other type comes from
an instance. From there, the chain flows down: a root spawns layouts, queries,
and mutations; a layout spawns pages; a base or plugin contributes shared
settings.
The same word opens and closes
A point is opened with .lets.<type>(...) and closed with .<type>(...) — the
same word at both ends:
root.lets.query(/* ... */).input(/* ... */).loader(/* ... */).query()
// ▲ open ▲ closeThe opener holds what comes before the point (a query's nothing, a page's
route, an action's method and path); the closer holds what comes at the end (a
mutation's react-query options, a page's component). Between them you add the
point's methods. The closing call must match the type you opened — a page closes
with .page(...), a mutation with .mutation(...). (An action is the
exception: it can close as .action(), .query(), or .mutation(), depending
on how you want to call it.)
The two ends have names worth fixing now, so the rest of the docs stay
consistent. Everything from the opener up to (and including) the closing call is
a stage-method — the chain methods you call while the point is still being
built (.input, .loader, .with, the closing .page(...), …). What you get
after the close is a ready-method — the surface on the finished point
(useQuery, fetchMutation, .route, .id, .useValue, …). The two states
even have their own types in the code: a point under construction is a
StagePoint, a closed one is a ReadyPoint.
const ideaQuery = root.lets
.query() // ┐
.input(z.object({ id: z.number() })) // stage-methods (building the point)
.loader(/* ... */) // ┘
.query() // close → from here on, ready-methods
ideaQuery.useQuery({ id: 1 }) // ready-method on the finished queryEverything lands in one collection
Because all eleven types are the same class, the compiler collects them into one array — the points collection the engine loads. Every point, whatever its type, sits in the same list:
// generated points file (shape)
export default [root, ideaScreenComponent, createIdeaMutation, ideaQuery] // PointsDefinitionEach point carries a stable id of the form scope:type:name (e.g.
root:query:idea). The engine wires this collection in; you never assemble it
by hand. See generator for how it's produced and
engine-config for how it's loaded.
Reference
Point types at a glance
| Type | Group | Opens off | Page |
|---|---|---|---|
page | mountable | root / base / layout | page |
layout | mountable | root / base / layout | layout |
component | mountable | root / base | component |
provider | mountable | root / base | provider |
query | data/endpoint | root / base | query |
infiniteQuery | data/endpoint | root / base | infinite-query |
mutation | data/endpoint | root / base | mutation |
action | data/endpoint | root / base | action |
root | structure | Point0 (static) / root | root |
base | structure | root / base | base |
plugin | structure | Point0 (static) | plugin |
query, mutation, and action are always HTTP endpoints; page, layout,
component, and provider are endpoints only with a server loader or SSR;
root, base, and plugin are never endpoints.
Notation at a glance
Short (.lets.page) | Full (.lets('page', …)) | |
|---|---|---|
| Needs the compiler | yes | no |
| Point name | read from the variable | passed explicitly |
| Runs at runtime as-is | no — rewritten | yes |
| Used in these docs | yes (default) | only where a name is pinned |
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️