Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Transformer

  • Раздел: Methods

On this page

  1. Root only
  2. Why you'd set it
  3. The default transformer
  4. How it bakes into the query key
  5. Not the same as the store transformers
  6. Reference
  7. Signature
  8. Where it runs
  9. Edge cases

.transformer sets how Point0 serializes data crossing the wire — query inputs and outputs, request bodies, the SSR dehydrated state. By default that's plain JSON, which can't carry a Date, Map, Set, or BigInt. Pass superjson and those types survive the round trip, on both ends and inside the query key.

import superjson from 'superjson'

export const root = Point0.lets
  .root()
  .serverUrl(sharedEnv.SERVER_URL)
  .clientUrl(sharedEnv.CLIENT_URL)
  .transformer(superjson) // Date / Map / Set / BigInt now round-trip
  .schemaHelper(zodSchemaHelper())
  .errorClass(AppError) // optional — your own error class; default is ErrorPoint0
  .root()

This is the canonical production wiring: one transformer on the root, applied to every point beneath it.

Stripping: .transformer is server-and-client — it's a root setter, kept on both bundles and never stripped, because serialization has to run identically on the server and in the browser.

Root only

.transformer is a root method — you call it on Point0.lets.root()… and nowhere else. It's not available on a page, query, mutation, action, or layout; writing it there is a type error. One root, one transformer, shared by every point in that root's scope.

superjson is not a Point0 dependency — install it yourself:

bun add superjson

.transformer takes any { serialize, deserialize } pair (see Reference); superjson satisfies that out of the box.

Why you'd set it

Without a transformer, the wire is plain JSON, and the type lies. A loader can return a Date, the type says Date, but what arrives on the client is a string:

export const ideaPage = root.lets
  .page('/ideas/:id')
  .loader(() => ({ createdAt: new Date('2026-01-01') }))
  .page(({ data }) => {
    // type says data.createdAt: Date
    // runtime (no transformer): data.createdAt === '2026-01-01T00:00:00.000Z' — a string
    return <time>{String(data.createdAt)}</time>
  })

This is the most common surprise. JSON.stringify turns a Date into an ISO string, drops a Map/Set to {}, and throws on a BigInt. The data degrades on the way out and never reconstructs on the way in.

Set .transformer(superjson) on the root and the same loader gives the page a real Date:

.page(({ data }) => {
  // runtime (with superjson): data.createdAt instanceof Date === true
  return <time>{data.createdAt.toISOString()}</time>
})

The same holds for request bodies. A BigInt in a mutation or action body survives both directions only when the root has a transformer:

// action body schema: z.object({ amount: z.bigint() })
await transferAction.fetch({ body: { amount: 100n } })
// server receives amount === 100n (with superjson) — not a string, not a throw

On the server, validation runs after the transformer deserializes, so a z.bigint() schema sees a real bigint and accepts it. With plain JSON the same schema would reject the value.

Point0 hands serialization to your transformer, so any type superjson supports — Date, BigInt, Map, Set, and more — round-trips. See superjson's supported types.

The default transformer

When you never call .transformer, Point0 uses a blank transformer: serialize/deserialize are identity (pass-through), and the wire format is plain JSON with stable key order (it stringifies via safe-stable-stringify, a Point0 runtime dependency):

// default transformer, serializing { date, string }:
'{"date":"2017-01-01T00:00:00.000Z","string":"value"}'
// keys sorted; the Date is already a string — its type is gone

superjson instead wraps the value in a { json, meta } envelope, where meta records which fields need reconstructing:

// superjson, same input:
'{"json":{"date":"2017-01-01T00:00:00.000Z","string":"value"},' +
  '"meta":{"v":1,"values":{"date":["Date"]}}}'
// meta.values.date = ["Date"] tells deserialize to rebuild a Date

That meta is what carries the type across the wire — and it's why the serialized form is larger than plain JSON.

How it bakes into the query key

The transformer is part of the cache key, not only the wire. A query key's input field is the transformer's stringified input:

ideaQuery.getQueryKey({ id: 123 })
// [
//   'point0',
//   { scope, type, name, mode, finiteness, tags, output,
//     input: '{"id":123}' }, // = transformer.stringify(routedInput)
// ]

input is safe-stable-stringify(transformer.serialize(routedInput)). With the default transformer that's plain stable JSON. With superjson, the superjson serialization (its { json, meta } shape, then stable-stringified) is what keys the cache — so an input containing a Date produces a distinct, reconstructable key instead of a lossy string.

Stable stringification means key order doesn't matter: { a, b } and { b, a } hit the same cache entry. See Query for the full key shape; for what counts as routedInput (page/layout search filtering, action sections), see Validation.

Because the transformer is in the key, changing it changes cache keys app-wide. If you add or swap .transformer in an existing app, keys built with the old transformer won't match the new ones, and Point0 does nothing to reconcile them. In-flight keys sort themselves out on the next fetch; persisted keys are your concern — if you persist the query cache through TanStack's persistQueryClient, change its buster string when you change the transformer so the stale cache is discarded.

Not the same as the store transformers

Two other surfaces take a transformer with the same { serialize, deserialize } shape, but they're separate knobs from the root .transformer:

  • CookieStore accepts a per-store transformer option.
  • SsrStore / SuperStore expose setTransformer(transformer).

Both default to the blank transformer independently. Setting .transformer on the root does not configure them, and vice versa.

Reference

Signature

.transformer(transformer: DataTransformer): this // root only, chainable

DataTransformer is the minimal pair both serialize and deserialize round through:

type DataTransformer = {
  serialize: (data: any) => any
  deserialize: (data: any) => any
}

Any object of that shape works — superjson is the recommended default, but a hand-rolled transformer is valid too. Point0 wraps whatever you pass to also derive a stable stringify/parse pair for query keys and the wire (stringify = safe-stable-stringify(serialize(data)), parse = deserialize(JSON.parse(str))).

Where it runs

BoundaryDirectionWhat it does
query / mutation / action requestclient → serverserialize the body
request parseserver readsdeserialize the body
query / loader outputserver → clientstringify the result
page dehydrated state (SSR)server → clientstringify the dehydrated state
response readclient readsdeserialize the JSON
query key inputbothstringify the routed input

Edge cases

  • serialize returning undefined is an error. If a custom transformer returns undefined for an input, Point0 throws Transformer returned undefined for input … on point … rather than sending an empty body.
  • Calling .transformer twice is last-wins. Each call overwrites the root's transformer with the new one — they don't compose — so the last .transformer in the chain is the one that runs.
НазадMapperДалееStage Methods

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter