Claude-skill-registry create-resource-service
Create a resource service for CRUD operations on domain entities. Use when creating services for entities like notes, users, courses that need data operations, authorization, and event emission. Triggers on "resource service", "entity service", "crud service", "note service", "create service for".
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/create-resource-service" ~/.claude/skills/majiayu000-claude-skill-registry-create-resource-service && rm -rf "$T"
manifest:
skills/data/create-resource-service/SKILL.mdsource content
Create Resource Service
Creates a service for CRUD operations on a domain entity. Resource services extend
BaseService for event emission, inject repositories for data access, and use AuthorizationService for permission checks.
Quick Reference
Location:
src/services/{entity-name}.service.ts
Naming: Singular, kebab-case (e.g., note.service.ts, course.service.ts)
Prerequisites
Before creating a resource service, ensure you have:
- Schema created (
)src/schemas/{entity-name}.schema.ts - Repository interface created (
)src/repositories/{entity-name}.repository.ts - At least one repository implementation (MockDB or MongoDB)
Instructions
Step 1: Create the Service File
Create
src/services/{entity-name}.service.ts
Step 2: Import Dependencies
import type { I{Entity}Repository } from "@/repositories/{entity-name}.repository"; import type { PaginatedResultType } from "@/schemas/shared.schema"; import type { Create{Entity}Type, {Entity}QueryParamsType, {Entity}Type, Update{Entity}Type, } from "@/schemas/{entity-name}.schema"; import type { AuthenticatedUserContextType } from "@/schemas/user.schemas"; import { AuthorizationService } from "@/services/authorization.service"; import { UnauthorizedError } from "@/errors"; import { MockDb{Entity}Repository } from "@/repositories/mockdb/{entity-name}.mockdb.repository"; import { BaseService } from "@/events/base.service";
Step 3: Create the Service Class
export class {Entity}Service extends BaseService { private readonly {entity}Repository: I{Entity}Repository; private readonly authorizationService: AuthorizationService; constructor( {entity}Repository?: I{Entity}Repository, authorizationService?: AuthorizationService, ) { super("{entities}"); // Service name for events (plural) this.{entity}Repository = {entity}Repository ?? new MockDb{Entity}Repository(); this.authorizationService = authorizationService ?? new AuthorizationService(); } // CRUD methods... }
Step 4: Implement CRUD Methods
getAll
async getAll( params: {Entity}QueryParamsType, user: AuthenticatedUserContextType, ): Promise<PaginatedResultType<{Entity}Type>> { // Admins see all, users see only their own if (this.authorizationService.isAdmin(user)) { return this.{entity}Repository.findAll(params); } return this.{entity}Repository.findAll({ ...params, createdBy: user.userId }); }
getById
async getById( id: string, user: AuthenticatedUserContextType, ): Promise<{Entity}Type | null> { const {entity} = await this.{entity}Repository.findById(id); if (!{entity}) { return null; } const canView = await this.authorizationService.canView{Entity}(user, {entity}); if (!canView) throw new UnauthorizedError(); return {entity}; }
create
async create( data: Create{Entity}Type, user: AuthenticatedUserContextType, ): Promise<{Entity}Type> { const canCreate = await this.authorizationService.canCreate{Entity}(user); if (!canCreate) throw new UnauthorizedError(); const {entity} = await this.{entity}Repository.create(data, user.userId); this.emitEvent("created", {entity}, { id: {entity}.id, user, }); return {entity}; }
update
async update( id: string, data: Update{Entity}Type, user: AuthenticatedUserContextType, ): Promise<{Entity}Type | null> { const {entity} = await this.{entity}Repository.findById(id); if (!{entity}) { return null; } const canUpdate = await this.authorizationService.canUpdate{Entity}(user, {entity}); if (!canUpdate) throw new UnauthorizedError(); const updated{Entity} = await this.{entity}Repository.update(id, data); if (!updated{Entity}) { return null; } this.emitEvent("updated", updated{Entity}, { id: updated{Entity}.id, user, }); return updated{Entity}; }
delete
async delete( id: string, user: AuthenticatedUserContextType, ): Promise<boolean> { const {entity} = await this.{entity}Repository.findById(id); if (!{entity}) { return false; } const canDelete = await this.authorizationService.canDelete{Entity}(user, {entity}); if (!canDelete) throw new UnauthorizedError(); const deleted = await this.{entity}Repository.remove(id); if (deleted) { this.emitEvent("deleted", {entity}, { id: {entity}.id, user, }); } return deleted; }
Patterns & Rules
Extending BaseService
export class {Entity}Service extends BaseService { constructor(...) { super("{entities}"); // Plural name for event namespace } }
The
serviceName is used for event routing (e.g., notes:created, notes:updated).
Dependency Injection
constructor( {entity}Repository?: I{Entity}Repository, authorizationService?: AuthorizationService, ) { // Provide defaults for convenience, but allow injection for testing this.{entity}Repository = {entity}Repository ?? new MockDb{Entity}Repository(); this.authorizationService = authorizationService ?? new AuthorizationService(); }
- Accept interfaces for repositories (not concrete classes)
- Provide defaults for easier instantiation
- Allow injection for testing with mocks
Authorization Pattern
Every operation should check permissions:
const canDoX = await this.authorizationService.canX{Entity}(user, {entity}); if (!canDoX) throw new UnauthorizedError();
You must add corresponding methods to
AuthorizationService:
canView{Entity}(user, entity)canCreate{Entity}(user)canUpdate{Entity}(user, entity)canDelete{Entity}(user, entity)
Event Emission Pattern
Emit events after successful operations:
this.emitEvent("created", {entity}, { id: {entity}.id, user }); this.emitEvent("updated", updated{Entity}, { id: updated{Entity}.id, user }); this.emitEvent("deleted", {entity}, { id: {entity}.id, user });
Events are only emitted for
create, update, delete - not for reads.
Error Handling
- Not found: Return
(let controller decide HTTP status)null - Unauthorized: Throw
fromUnauthorizedError@/errors - Other errors: Let them propagate (global error handler catches)
Return Types
:getAllPromise<PaginatedResultType<{Entity}Type>>
:getByIdPromise<{Entity}Type | null>
:createPromise<{Entity}Type>
:updatePromise<{Entity}Type | null>
:deletePromise<boolean>
Complete Example
See REFERENCE.md for a complete
NoteService implementation.
After Creating the Service
- Add authorization methods to
for this entityAuthorizationService - Add event schema (optional) - see
skilladd-resource-events - Create controller - see
skillcreate-controller - Write tests - see
skilltest-service
What NOT to Do
- Do NOT put HTTP-specific logic in services (that's for controllers)
- Do NOT return HTTP status codes or responses
- Do NOT skip authorization checks
- Do NOT emit events before confirming the operation succeeded
- Do NOT inject concrete repository classes - use interfaces