This document describes coding standards for Laioutr apps — Nuxt modules that extend Laioutr via registerLaioutrApp and optionally provide orchestr handlers, sections, blocks, and page wrappers. Following these conventions keeps your app consistent with official and community apps, simplifies reviews, and makes it easier for others to contribute.
The standards cover: package and naming, module structure, configuration, orchestr layout and patterns, runtime layout, TypeScript, build and tooling, testing, and linting/formatting. Where the ecosystem allows flexibility, we note it.
package.json must be unique and stable (e.g. my-laioutr-app, @laioutr-app/shopify). Use a scope (e.g. @laioutr-app/, @laioutr-org/) for published apps.laioutrrc.json by this key. In module.ts use configKey: name where name is imported from package.json.// module.ts
import { name, version } from '../package.json';
export default defineNuxtModule<ModuleOptions>({
meta: {
name,
version,
configKey: name, // must match package name
},
// ...
});
PublicRuntimeConfig and RuntimeConfig with the same key (e.g. ['my-laioutr-app'] or ['@laioutr-app/commercetools']) so TypeScript knows your app’s config shape.package.json. Use a CHANGELOG (e.g. changelogen) for release notes.src/module.ts is the module entry. Build output is typically dist/module.mjs and dist/types.d.mts.package.json, expose the main entry and types. Re-export types or utilities from module.ts if needed (e.g. export * from "./globalExtensions")."files": ["dist"] so source and playgrounds are not published.name, version, and configKey (equal to package name).async setup(options, nuxt) where you:
createResolver(import.meta.url) and a helper like resolveRuntimeModule(path) => resolve("./runtime", path).nuxt.options.build.transpile.push(resolve("./runtime")).registerLaioutrApp with name, version, and the appropriate dirs (orchestrDirs, sections, blocks, pageWrapper).nuxt.options._prepare, call installModule for peer dependencies so auto-imports and aliases work in consuming apps.package.json.[resolveRuntimeModule("server/orchestr")]).PublicRuntimeConfig[packageName] and RuntimeConfig[packageName] using the same key as in module.ts. Use the types you export from module.ts (RuntimeConfigModulePublic, RuntimeConfigModulePrivate).GlobalComponents and ComponentCustomProperties only if your app adds global components or properties.export {} so it is treated as a module.nuxt.config or laioutrrc.json). Document each property; use optional (?) only when the option has a default or is truly optional.ModuleOptions or includes secrets (API keys, client secrets).defu to merge user options with defaults into both nuxt.options.runtimeConfig[name] and nuxt.options.runtimeConfig.public[name]. Only put public values in public; keep secrets in private config.my-laioutr-app) so Laioutr can pass the correct slice of laioutrrc.json into your module. See App Configuration.src/runtime/server/middleware/ (e.g. defineCommercetools.ts or index.ts).defineOrchestr.meta({ app: name }).extendRequest(...). In extendRequest, build any client/context (e.g. API client, auth, facets) and return { context: { ... } }. Use name from package.json.defineXQuery = base .queryHandlerdefineXAction = base .actionHandlerdefineXLink = base .linkHandlerdefineXComponentResolver = base .componentResolverdefineXQueryTemplateProvider = base .queryTemplateProvider (if used)defineCommercetoolsQuery, defineEmporixAction) and use the canonical types from @laioutr-core/canonical-types.cart/, menu/, product/, product-variant/, newsletter/, etc.<name>.query.ts (e.g. by-slug.query.ts, get-current.query.ts, by-alias.query.ts).<name>.action.ts (e.g. add-to-cart.action.ts, add-item.action.ts, subscribe.action.ts).<name>.link.ts (e.g. variants.link.ts).base.resolver.ts per entity (e.g. cart/base.resolver.ts, product/base.resolver.ts). Use entityType, label, provides, and resolve with $entity.<name>.template.ts (e.g. by-alias.template.ts) when you provide multiple query inputs for static/menu generation.orchestr/plugins/ (e.g. zodFix.ts for Zod compatibility).errors/ in the relevant entity folder (e.g. menu/errors/category-not-found.error.ts, product/errors/products-not-found.error.ts). Use a .error.ts suffix.defineXQuery(CanonicalQuery, async ({ context, input, clientEnv, filter, sorting, pagination, passthrough }) => { ... }). Return the shape expected by the canonical type (e.g. { id }, { ids, total, availableFilters, availableSortings }).defineXAction(CanonicalAction, async ({ context, input, clientEnv }) => { ... }). Perform side effects and return the canonical action result.defineXLink(CanonicalLink, async ({ entityIds, context, passthrough }) => { ... }). Return { links: [{ sourceId, targetIds }] }.defineXComponentResolver({ entityType, label, provides: [...], resolve: async ({ entityIds, context, clientEnv, $entity, passthrough }) => { ... } }). Use $entity({ id, base: () => ({...}), ... }) to build entities; return { entities }.@laioutr-core/canonical-types (e.g. ecommerce, entity/cart, entity/product). Do not invent new variable or entity shapes; extend the canonical model if needed via the proper channels.@ebec/http). Extend NotFoundError, BadRequestError, etc., for consistent API behavior.code (e.g. static readonly code = "PRODUCTS_NOT_FOUND") and pass a clear message and data in the constructor.@public if they are part of your app’s API.export default () => {}) from error files if the file is only for the class and you want to avoid side effects when importing.createPassthroughToken<T>(key) from the orchestr for data that should be shared between a query/link and a resolver in the same request (e.g. categories, products, variants). Set and get via passthrough.set(token, value) and passthrough.get(token).runtime/server/orchestr-helper/ (e.g. cart helpers, product mappers, localized getters) so handlers stay thin and testable.runtime/server/mappers/ and import them in resolvers or handlers.src/runtime/server/: client/ (API/SDK factory), const/ (keys, tokens), mappers/, middleware/ (orchestr defineOrchestr), orchestr/, orchestr-helper/, utils/.src/runtime/app/: components/, sections/, blocks/, and optionally image/ (providers), public/.resolve("./runtime") to nuxt.options.build.transpile so the runtime is compiled by the consuming app.@laioutr-core/canonical-types and @laioutr-core/core-types for orchestr inputs and outputs.RuntimeConfigModulePublic), you may need // eslint-disable-line @typescript-eslint/no-empty-object-type or a comment property to satisfy the linter; keep the type if it is used for augmentation.import type where only types are needed to keep runtime imports clear.nuxt-module-build). Externalize dependencies that must not be bundled: at least defu, and often @laioutr-core/frontend-core, @laioutr-core/kit, @parcel/watcher if used. This keeps the dist small and avoids duplicate instances of core packages.nuxi dev playground).nuxi dev orchestr-playground).dev and orchestr-dev.nuxt-module-build build).eslint .).vitest run).vue-tsc --noEmit).laioutrrc.json and Frontend Core. Use it for full UI and integration testing.package.json.test/ (e.g. basic.test.ts) and fixtures under test/fixtures/basic/ (minimal Nuxt app that uses your module).@laioutr/eslint-config/nuxt-module). Run pnpm lint (or npm run lint) before committing and in CI.@laioutr/prettier-config or a local .prettierrc with tabWidth: 2, useTabs: false, printWidth: 80 or 120). Format on save or in pre-commit.RuntimeConfigModulePublic.