С любовью ко всем разработчикам на свете ❤️
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 itThe rest of this page lists every point type and explains the two ways to write
.lets.
There are eleven. They split into three groups by what they do.
Mountables — they render UI.
.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.
POST
endpoint and a TanStack Mutation in one.Response.Structure — they hold defaults and shape the tree.
.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.
.lets notationEvery example in these docs opens a point with the short form —
root.lets.page('/ideas'), root.lets.query(), root.lets.mutation(). The
point's type is the method name; 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 compiler.lets(type, name, …) formThe 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 example apps use the full form
in a few places where a name is pinned explicitly — most of all the expo
example, which doesn't run generate.
.letsMost 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.
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 the rest of the docs rely on. 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 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 queryBecause all eleven types are the same class, the compiler collects them into one array — the points collection the engine loads:
// 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.
| 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.
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 |