Vendure Best Practices
Apply these guidelines when implementing or reviewing Vendure code, plugins, or React dashboard extensions. They cover code structure, event handling, logging, and other implementation constraints.
General guidelines
Section titled “General guidelines”- Place resolvers only in
resolvers: [], not inproviders: []. - Make behaviour configurable through a strategy when consumers may reasonably need to change it; avoid speculative abstractions. For example, expose
sendToAnalytics: (order) => ...so consumers control how orders are formatted and sent. - Do not update order custom fields in
OrderPlacedEventhandlers. Concurrent updates by Vendure and the handler can cause race conditions, leaving orders unsettled or active. - Always prefer built-in or ready-made config, strategies, services, React components, etc. supplied by Vendure over creating new custom code, even if that means sacrificing some functionality.
Vendure code structure
Section titled “Vendure code structure”api/— GraphQL schema extensions (api-extensions.ts), resolvers and controllersconfig/— Vendure configuration, strategies, promotions and shipping strategies. Anything that goes invendureConfiggoes here.entities/— TypeORM entity definitionsservices/— Business logic (NestJS @Injectable() services)types.ts— Plugin options interface and shared type definitionsconstants.ts— Injection token symbols, logger context<plugin-name>.plugin.ts— @VendurePlugin decorator that registers everything
No async/await in eventBus.subscribe()
Section titled “No async/await in eventBus.subscribe()”NEVER use async/await inside eventBus.ofType(*ProductEvent*).subscribe(. Always wrap the function inside in a .catch.
// NEVER do this, badeventBus.ofType(*ProductEvent*).subscribe(async () => await someAsyncFunction();)
// Do this instead, this is goodeventBus.ofType(*ProductEvent*).subscribe(async () => someAsyncFunction().catch((e) => Logger.error(`Failed: ${e.message}`, loggerCtx, e.stack));)Logger
Section titled “Logger”- Use the Vendure built-in logger
- Include
order.codein order-related messages for traceability; use identifiers such asproduct.idorvariant.skufor other entities. - Include
e.stackfor errors
Logger.error(`It failed for order ${order.code}`, loggerCtx, e.stack); // Always use the nearest loggerCtx you can findLogger.info(`Order ${order.code} has been processed`, loggerCtx);Testing
Section titled “Testing”- Prefer GraphQL queries and mutations; call services directly when this makes cases more readable or isolated.
- Never use arbitrary sleeps such as waiting ten seconds. Wait for an observable condition, for example
await waitFor(() => someCondition === true), using the test framework’s bounded condition-wait helper. - Always use
gqlfromgraphql-tagfor GraphQL documents in tests. Never use/* GraphQL */template literal comments. - Place graphql queries at the bottom of the file, below test cases.
Vendure documentation
Section titled “Vendure documentation”Always search the Vendure docs for existing configurations and strategies before implementing your own.
- Use the
mcp__vendure_docs__search_docstool to search the Vendure docs. Notify me when I dont have it. - Prefer the MCP tool for any Vendure-specific concepts (configuration, strategies, plugins, GraphQL, etc.).
