vendure-plugin-writing
Vendure Plugin Writing
Purpose
Guide creation of Vendure plugins following official best practices and production patterns.
When NOT to Use
- Reviewing existing plugins (use vendure-plugin-reviewing)
- GraphQL-specific work (use vendure-graphql-writing)
- Entity definition only (use vendure-entity-writing)
- Admin UI only (use vendure-admin-ui-writing)
FORBIDDEN Patterns
- Missing @VendurePlugin decorator
- Not importing PluginCommonModule
- Not using @Injectable on services
- Hardcoded values instead of plugin configuration
- Direct database access bypassing services
- Missing async/await on lifecycle hooks
- Constructor injection in strategies (use init() method)
- Skipping default values for optional config
REQUIRED Patterns
- @VendurePlugin() decorator with proper metadata
- PluginCommonModule in imports array
- Configuration interface with sensible defaults
- Static init() method for plugin configuration
- @Injectable() decorator on all services
- Proper DI via constructor injection in services
- Lifecycle hooks when needed (onApplicationBootstrap)
Workflow
Step 1: Define Plugin Configuration Interface
// my-plugin.types.ts
export interface MyPluginOptions {
enabled?: boolean;
apiKey?: string;
maxRetries?: number;
}
// Default configuration
export const defaultMyPluginOptions: Required<MyPluginOptions> = {
enabled: true,
apiKey: "",
maxRetries: 3,
};
Step 2: Create the Plugin Class
// my-plugin.plugin.ts
import { PluginCommonModule, VendurePlugin } from "@vendure/core";
import { MyService } from "./my.service";
import { MyEntity } from "./my.entity";
import { MyResolver } from "./my.resolver";
import { MyPluginOptions, defaultMyPluginOptions } from "./my-plugin.types";
@VendurePlugin({
imports: [PluginCommonModule],
providers: [MyService],
entities: [MyEntity],
adminApiExtensions: {
schema: graphqlAdminSchema,
resolvers: [MyResolver],
},
})
export class MyPlugin {
static options: Required<MyPluginOptions>;
static init(options: MyPluginOptions = {}): typeof MyPlugin {
this.options = {
...defaultMyPluginOptions,
...options,
};
return MyPlugin;
}
}
Step 3: Create Injectable Service
// my.service.ts
import { Injectable } from "@nestjs/common";
import { TransactionalConnection, RequestContext } from "@vendure/core";
import { MyEntity } from "./my.entity";
@Injectable()
export class MyService {
constructor(private connection: TransactionalConnection) {}
async findAll(ctx: RequestContext): Promise<MyEntity[]> {
return this.connection.getRepository(ctx, MyEntity).find();
}
async create(ctx: RequestContext, input: CreateInput): Promise<MyEntity> {
const entity = new MyEntity(input);
return this.connection.getRepository(ctx, MyEntity).save(entity);
}
}
Step 4: Add Lifecycle Hooks (If Needed)
import { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common";
import { Injector } from "@vendure/core";
@VendurePlugin({
// ... metadata
})
export class MyPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
static options: Required<MyPluginOptions>;
constructor(private injector: Injector) {}
async onApplicationBootstrap(): Promise<void> {
// Initialize strategies, start background tasks, etc.
if (MyPlugin.options.enabled) {
const service = this.injector.get(MyService);
await service.initialize();
}
}
async onApplicationShutdown(): Promise<void> {
// Cleanup resources
}
}
Step 5: Register Plugin in vendure-config.ts
import { MyPlugin } from "./plugins/my-plugin/my-plugin.plugin";
export const config: VendureConfig = {
plugins: [
MyPlugin.init({
enabled: true,
apiKey: process.env.MY_API_KEY,
maxRetries: 5,
}),
],
};
Plugin Structure Template
my-plugin/
├── my-plugin.plugin.ts # Main plugin file
├── my-plugin.types.ts # Configuration types
├── my.service.ts # Business logic
├── my.entity.ts # Database entity
├── my.resolver.ts # GraphQL resolver
├── schema.ts # GraphQL schema extensions
├── strategies/ # Custom strategies
│ └── my.strategy.ts
├── ui/ # Admin UI extensions
│ └── providers.ts
└── index.ts # Public exports
Examples
Example 1: Simple Analytics Plugin
import {
PluginCommonModule,
VendurePlugin,
EventBus,
ProductEvent,
} from "@vendure/core";
import { OnApplicationBootstrap } from "@nestjs/common";
interface AnalyticsOptions {
trackingId: string;
}
@VendurePlugin({
imports: [PluginCommonModule],
})
export class AnalyticsPlugin implements OnApplicationBootstrap {
static options: AnalyticsOptions;
constructor(private eventBus: EventBus) {}
static init(options: AnalyticsOptions): typeof AnalyticsPlugin {
this.options = options;
return AnalyticsPlugin;
}
async onApplicationBootstrap(): Promise<void> {
this.eventBus.ofType(ProductEvent).subscribe((event) => {
console.log(`Product ${event.type}: ${event.entity.name}`);
// Send to analytics service
});
}
}
Example 2: Plugin with Custom Strategy
import { PluginCommonModule, VendurePlugin, Injector } from "@vendure/core";
import { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common";
import { MyStrategy, DefaultMyStrategy } from "./strategies/my.strategy";
interface PluginOptions {
strategy?: MyStrategy;
}
@VendurePlugin({
imports: [PluginCommonModule],
})
export class StrategyPlugin
implements OnApplicationBootstrap, OnApplicationShutdown
{
static options: Required<PluginOptions>;
constructor(private injector: Injector) {}
static init(options: PluginOptions = {}): typeof StrategyPlugin {
this.options = {
strategy: new DefaultMyStrategy(),
...options,
};
return StrategyPlugin;
}
async onApplicationBootstrap(): Promise<void> {
// Initialize strategy with injector for DI
if (typeof StrategyPlugin.options.strategy.init === "function") {
await StrategyPlugin.options.strategy.init(this.injector);
}
}
async onApplicationShutdown(): Promise<void> {
if (typeof StrategyPlugin.options.strategy.destroy === "function") {
await StrategyPlugin.options.strategy.destroy();
}
}
}
Example 3: Plugin with Admin/Shop API Extensions
import { PluginCommonModule, VendurePlugin } from "@vendure/core";
import { gql } from "graphql-tag";
import { MyService } from "./my.service";
import { MyAdminResolver, MyShopResolver } from "./my.resolver";
const graphqlAdminSchema = gql`
extend type Query {
myAdminData: [MyType!]!
}
extend type Mutation {
updateMyData(input: UpdateInput!): MyType!
}
`;
const graphqlShopSchema = gql`
extend type Query {
myPublicData: [MyType!]!
}
`;
@VendurePlugin({
imports: [PluginCommonModule],
providers: [MyService],
adminApiExtensions: {
schema: graphqlAdminSchema,
resolvers: [MyAdminResolver],
},
shopApiExtensions: {
schema: graphqlShopSchema,
resolvers: [MyShopResolver],
},
})
export class ApiPlugin {}
Common Patterns
Accessing Plugin Options in Service
@Injectable()
export class MyService {
get options() {
return MyPlugin.options;
}
async doSomething() {
if (this.options.enabled) {
// Use this.options.apiKey
}
}
}
Custom Fields Configuration
@VendurePlugin({
imports: [PluginCommonModule],
configuration: (config) => {
config.customFields.Product.push({
name: "myCustomField",
type: "string",
label: [{ languageCode: "en", value: "My Custom Field" }],
});
return config;
},
})
export class CustomFieldsPlugin {}
Plugin Dependencies
// If plugin depends on another plugin's services
@VendurePlugin({
imports: [PluginCommonModule, OtherPlugin],
providers: [MyService],
})
export class DependentPlugin {}
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Service not found | Not in providers array | Add to @VendurePlugin({ providers: [] }) |
| Entity not created | Not in entities array | Add to @VendurePlugin({ entities: [] }) |
| Options undefined | init() not called | Call MyPlugin.init() in vendure-config.ts |
| Circular dependency | Improper imports | Use forwardRef() or restructure modules |
| Strategy not initialized | Missing init() call | Implement OnApplicationBootstrap |
Related Skills
- vendure-plugin-reviewing - Audit plugin for violations
- vendure-graphql-writing - GraphQL schema and resolvers
- vendure-entity-writing - Database entities
- vendure-admin-ui-writing - Admin UI extensions
External Documentation
Context7
mcp__context7__get-library-docs
context7CompatibleLibraryID: "/vendure-ecommerce/vendure"
topic: "plugins"
Official Docs
More from meriley/claude-code-skills
obs-cpp-qt-patterns
C++ and Qt integration patterns for OBS Studio plugins. Covers Qt6 Widgets for settings dialogs, CMAKE_AUTOMOC, OBS frontend API, optional Qt builds with C fallbacks, and modal dialog patterns. Use when adding UI components or C++ features to OBS plugins.
54vendure-developing
Develop Vendure e-commerce plugins, extend GraphQL APIs, create Admin UI components, and define database entities. Use vendure-expert agent for comprehensive guidance across all Vendure development domains.
35vendure-admin-ui-writing
Create Vendure Admin UI extensions with React components, route registration, navigation menus, and GraphQL integration. Handles useQuery, useMutation, useInjector patterns. Use when building Admin UI features for Vendure plugins.
32vendure-entity-writing
Define Vendure database entities extending VendureEntity, with TypeORM decorators, relations, custom fields, and channel-awareness. Use when creating database models in Vendure.
30vendure-graphql-writing
Extend Vendure GraphQL schema with custom types, queries, mutations, and resolvers. Handles RequestContext threading, permissions, and dual Shop/Admin API separation. Use when adding GraphQL endpoints to Vendure.
30vendure-admin-ui-reviewing
Review Vendure Admin UI extensions for React pattern violations, missing hooks, improper state management, and UI anti-patterns. Use when reviewing Admin UI PRs or auditing UI quality.
25