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
$dataknow 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.
Recommended directory shape (datatype-oriented) β
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.*.tsYou 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.
| Topic | Page |
|---|---|
Plugin index.ts | Plugin index (index.ts) |
| Persistence | Model |
| Validation and types | Schema |
| Business logic | Service |
| Payload and related helpers | Helpers |
HTTP surface and route config | Routes |
| Frontend generation overrides | Frontend 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.tsExample index.ts (imports + plugin function + registrations):
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