skills/foundation/umbraco-repository-pattern/SKILL.md
Understand and use repositories in Umbraco backoffice (foundational concept)
npx skillsauth add albanist/umbraco_cli umbraco-repository-patternInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Repositories are the Backoffice's entry point for data requests and update notifications, abstracting data access from various sources (server, offline database, store, Signal-R). They provide a structured way to manage data operations, separating business logic from direct data access for easier maintenance and scalability. Repositories use data sources behind the scenes, allowing consumers to work with data without knowing where or how it's stored.
NEVER use raw fetch() for custom API calls. This will result in 401 Unauthorized errors.
ALWAYS use a generated OpenAPI client configured with Umbraco's auth context. See the umbraco-openapi-client skill for setup instructions.
Always fetch the latest docs before implementing:
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { MyRepository } from './my-repository.js';
const repositoryManifest = {
type: 'repository',
alias: 'My.Repository',
name: 'My Repository',
api: MyRepository,
};
umbExtensionsRegistry.register(repositoryManifest);
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
// Import from generated OpenAPI client (see umbraco-openapi-client skill)
import { MyExtensionService } from '../api/index.js';
export class MyRepository extends UmbControllerBase {
constructor(host: UmbControllerHost) {
super(host);
}
async getAll() {
// Use generated OpenAPI client - handles auth automatically
const response = await MyExtensionService.getAll();
return response.data;
}
async getById(id: string) {
const response = await MyExtensionService.getById({ path: { id } });
return response.data;
}
async create(data: any) {
const response = await MyExtensionService.create({ body: data });
return response.data;
}
async update(id: string, data: any) {
const response = await MyExtensionService.update({
path: { id },
body: data,
});
return response.data;
}
async delete(id: string) {
await MyExtensionService.delete({ path: { id } });
}
}
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { MyRepository } from './my-repository.js';
export class MyContext extends UmbContextBase<MyContext> {
#repository = new MyRepository(this);
#items = new UmbArrayState([]);
readonly items = this.#items.asObservable();
async loadItems() {
const data = await this.#repository.getAll();
this.#items.setValue(data);
}
async createItem(item: any) {
const newItem = await this.#repository.create(item);
this.#items.setValue([...this.#items.getValue(), newItem]);
}
async deleteItem(id: string) {
await this.#repository.delete(id);
this.#items.setValue(
this.#items.getValue().filter(item => item.id !== id)
);
}
}
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { MyDataSource } from './my-data-source.js';
export class MyRepository extends UmbControllerBase {
#dataSource = new MyDataSource(this);
async getAll() {
return await this.#dataSource.getAll();
}
async getById(id: string) {
return await this.#dataSource.getById(id);
}
}
// Data Source - uses generated OpenAPI client
import { MyExtensionService } from '../api/index.js';
export class MyDataSource extends UmbControllerBase {
async getAll() {
// Use OpenAPI client - NEVER raw fetch()
const response = await MyExtensionService.getAll();
return response.data;
}
async getById(id: string) {
const response = await MyExtensionService.getById({ path: { id } });
return response.data;
}
}
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbTreeDataSource } from '@umbraco-cms/backoffice/tree';
export class MyTreeRepository extends UmbControllerBase {
#dataSource: UmbTreeDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#dataSource = new MyTreeDataSource(this);
}
async getRootItems() {
return await this.#dataSource.getRootItems();
}
async getChildrenOf(parentKey: string) {
return await this.#dataSource.getChildrenOf(parentKey);
}
async getAncestorsOf(key: string) {
return await this.#dataSource.getAncestorsOf(key);
}
}
Repository: Entry point for data operations, abstracts data access
Data Source: Underlying mechanism that actually fetches/stores data
Store: Optional layer that holds data throughout the session using State objects
Host Element: Repository must be instantiated with a host for proper context rendering
Separation of Concerns:
Benefits:
Usage Pattern:
That's it! Always fetch fresh docs, keep examples minimal, generate complete working code.
tools
Umbraco Automate operations (event-driven workflow automation)
development
Webhook management (the Management API's outbound event notifications)
development
Backoffice user management (accounts, state, groups, API credentials)
tools
Backoffice user group management (permission sets)