Skip to content

Golden Path β€” Backend overview ​

The Golden Path describes how we recommend structuring backend plugin code so it stays maintainable, works well with generated frontend config, and is easy for tooling and LLMs to extend.

Following this layout also makes future framework migrations easier: more boilerplate stays inside raclette’s registration APIs instead of ad-hoc patterns in each plugin, and the documentation and generators are aligned with this structure first. If you need to adapt to new persistence or generation behavior, plugins that stick to the Golden Path tend to require smaller, more localized changes.

What is a datatype? ​

A datatype is one domain unit your plugin owns: a noun with a lifecycle in your app (todo, project, flavor, invoice, …). It is not a framework keyword β€” it is an organizing idea.

In practice a datatype usually corresponds to:

  • one primary persistence model (if you persist it),
  • one service that owns reads/writes and side effects,
  • one set of routes under a consistent URL prefix,
  • one payload registration key so the UI and $data know how to treat list/detail views,
  • optional TypeBox schemas for request/response validation and shared typing.

Splitting by datatype keeps each folder or module small enough to reason about and mirrors how the core backend plugin wires multiple datatypes from one plugin entry (see @raclettejs/core/services/backend/src/corePlugins/raclette__core/backend/index.ts in the monorepo).

Why we recommend datatype-shaped folders ​

  • Clear ownership β€” all logic for one concept lives together, not scattered across a giant routes.ts.
  • Matches generated frontend models β€” route registration and naming feed generated-config.ts; consistent naming reduces surprises.
  • Scales with LLM edits β€” smaller files with predictable names are easier to regenerate or patch safely.

The framework does not forbid a flat layout (for example a single routes.ts and a single index.ts). That can be fine for a tiny plugin. For anything that will grow, prefer the layout below.

Same idea as the earlier single-page backend guide, but with an explicit per-datatype folder (name is yours; example is illustrative):

plugins/
└── your_plugin_name/
    └── backend/
        β”œβ”€β”€ index.ts                    # wires datatypes (only required backend file)
        └── datatypes/
            └── example/
                β”œβ”€β”€ index.ts          # optional: register this datatype in one place
                β”œβ”€β”€ example.model.ts
                β”œβ”€β”€ example.schema.ts
                β”œβ”€β”€ example.service.ts
                β”œβ”€β”€ helpers/
                β”‚   └── payload.ts
                └── routes/
                    β”œβ”€β”€ index.ts
                    └── route.example.*.ts

You can omit the inner datatypes/example/index.ts and register everything from the top-level backend/index.ts instead; the tree above is the shape we recommend once a plugin grows.

Page map ​

Read Backend overview first if you have not yet.

TopicPage
Plugin index.tsPlugin index (index.ts)
PersistenceModel
Validation and typesSchema
Business logicService
Payload and related helpersHelpers
HTTP surface and route configRoutes
Frontend generation overridesFrontend entity mapping

Minimal layout (possible, not the default) ​

If you intentionally keep everything flat, you still use the same registration calls from a single backend/index.ts, but you lose the separation benefits above. Use this when the plugin is truly small and unlikely to gain more entities.

Directory shape (minimal):

plugins/
└── your_plugin_name/
    └── backend/
        β”œβ”€β”€ index.ts
        β”œβ”€β”€ routes/
        β”‚   └── index.ts
        └── helpers/
            └── payload.ts

Example index.ts (imports + plugin function + registrations):

typescript
import type {
  PluginFastifyInstance,
  PluginOptions,
} from "@raclettejs/core/backend"
import { registerRoutes } from "./routes"
import { registerPayload } from "./helpers/payload"
import { registerExampleSchemas } from "./example.schema"

const examplePlugin = async (
  fastify: PluginFastifyInstance,
  _opts: PluginOptions,
) => {
  await fastify.register((instance) => registerRoutes(instance))

  registerPayload(fastify)

  registerExampleSchemas(fastify)
}

export default examplePlugin