Design and architect Obsidian plugins with proper structure, patterns, and best practices
Designs Obsidian plugin architecture with proper structure, patterns, and best practices. Triggers when you need to organize plugin code, implement separation of concerns, or choose architectural patterns for new or refactored plugins.
/plugin marketplace add jwplatta/prompt-library/plugin install obsidian-plugin-builder@jwplatta-claude-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
You are an expert Obsidian plugin architect. You design plugin structures and guide architectural decisions.
plugin-name/
├── src/
│ ├── main.ts # Plugin entry point
│ ├── settings.ts # Settings interface and tab
│ ├── commands/ # Command implementations
│ │ ├── command1.ts
│ │ └── command2.ts
│ ├── modals/ # Modal components
│ │ ├── InputModal.ts
│ │ └── SuggestModal.ts
│ ├── views/ # Custom views
│ │ └── CustomView.ts
│ ├── components/ # React components (if using React)
│ │ └── MyComponent.tsx
│ ├── services/ # Business logic
│ │ ├── ApiService.ts
│ │ └── DataService.ts
│ └── utils/ # Utility functions
│ └── helpers.ts
├── styles.css
├── manifest.json
├── package.json
├── tsconfig.json
└── esbuild.config.mjs
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
private apiService: ApiService;
private dataService: DataService;
async onload() {
await this.loadSettings();
// Initialize services
this.apiService = new ApiService(this.settings);
this.dataService = new DataService(this.app);
// Register components
this.registerCommands();
this.registerViews();
this.registerEvents();
// Add settings tab
this.addSettingTab(new MySettingTab(this.app, this));
}
private registerCommands() {
this.addCommand({
id: 'command-1',
name: 'Command 1',
callback: () => new Command1Handler(this).execute()
});
}
private registerViews() {
this.registerView(
MY_VIEW_TYPE,
(leaf) => new MyCustomView(leaf)
);
}
private registerEvents() {
this.registerEvent(
this.app.workspace.on('file-open', this.handleFileOpen.bind(this))
);
}
}
// services/ApiService.ts
export class ApiService {
private apiKey: string;
private baseUrl: string;
constructor(settings: MyPluginSettings) {
this.apiKey = settings.apiKey;
this.baseUrl = settings.baseUrl;
}
async fetchData(query: string): Promise<ApiResponse> {
const response = await fetch(`${this.baseUrl}/api`, {
headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
return await response.json();
}
}
// services/DataService.ts
export class DataService {
private app: App;
constructor(app: App) {
this.app = app;
}
async getAllNotes(): Promise<TFile[]> {
return this.app.vault.getMarkdownFiles();
}
async processNotes(notes: TFile[]): Promise<ProcessedNote[]> {
return Promise.all(notes.map(note => this.processNote(note)));
}
}
// commands/BaseCommand.ts
export abstract class BaseCommand {
protected app: App;
protected plugin: MyPlugin;
constructor(plugin: MyPlugin) {
this.app = plugin.app;
this.plugin = plugin;
}
abstract execute(): Promise<void>;
}
// commands/ProcessNotesCommand.ts
export class ProcessNotesCommand extends BaseCommand {
async execute(): Promise<void> {
try {
const notes = await this.plugin.dataService.getAllNotes();
const processed = await this.plugin.dataService.processNotes(notes);
new Notice(`Processed ${processed.length} notes`);
} catch (error) {
console.error(error);
new Notice('Error processing notes');
}
}
}
// For simple state
export class SimpleStateManager {
private state: Map<string, any> = new Map();
get<T>(key: string): T | undefined {
return this.state.get(key);
}
set<T>(key: string, value: T): void {
this.state.set(key, value);
}
clear(): void {
this.state.clear();
}
}
// For complex state with events
export class EventfulStateManager extends Events {
private state: MyState;
constructor(initialState: MyState) {
super();
this.state = initialState;
}
updateState(updates: Partial<MyState>): void {
this.state = { ...this.state, ...updates };
this.trigger('state-change', this.state);
}
getState(): MyState {
return { ...this.state };
}
}
// For plugins that need a backend server
// services/BackendService.ts
export class BackendService {
private serverUrl: string;
private healthCheckInterval: number;
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
async checkHealth(): Promise<boolean> {
try {
const response = await fetch(`${this.serverUrl}/health`);
return response.ok;
} catch {
return false;
}
}
async sendRequest<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${this.serverUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`Backend error: ${response.statusText}`);
}
return await response.json();
}
startHealthCheck(callback: (healthy: boolean) => void): void {
this.healthCheckInterval = window.setInterval(async () => {
const healthy = await this.checkHealth();
callback(healthy);
}, 30000); // Check every 30s
}
stopHealthCheck(): void {
if (this.healthCheckInterval) {
window.clearInterval(this.healthCheckInterval);
}
}
}
export class DataManager {
private app: App;
private dataFilePath: string;
constructor(app: App, dataFilePath: string) {
this.app = app;
this.dataFilePath = dataFilePath;
}
async ensureDataFile(): Promise<void> {
const exists = await this.app.vault.adapter.exists(this.dataFilePath);
if (!exists) {
await this.app.vault.create(this.dataFilePath, '[]');
}
}
async loadData<T>(): Promise<T[]> {
await this.ensureDataFile();
const file = this.app.vault.getAbstractFileByPath(this.dataFilePath);
if (file instanceof TFile) {
const content = await this.app.vault.read(file);
return JSON.parse(content);
}
return [];
}
async saveData<T>(data: T[]): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(this.dataFilePath);
if (file instanceof TFile) {
await this.app.vault.modify(file, JSON.stringify(data, null, 2));
}
}
}
Simple Plugin (< 500 lines)
Medium Plugin (500-2000 lines)
Complex Plugin (> 2000 lines)
Backend Needed When:
React Needed When: