Skip to content
create-utility-service logo

Create Utility Service

create-utility-service

Create a utility service for cross-cutting concerns. Use when creating services for authentication, authorization, email, notifications, or other shared functionality that doesn't directly map to a domain entity.

SKILL.md

Full skill instructions

Create Utility Service

Creates a service for cross-cutting concerns or specialized functionality. Unlike resource services, utility services don't extend BaseService and typically don't inject repositories.

Quick Reference

Location: src/​services/​{service-name}.service.ts Naming: Descriptive, kebab-case (e.g., authentication.service.ts, email.service.ts)

When to Use

Use this skill when creating services that:

  • Call external APIs (auth service, payment gateway, email provider)
  • Provide shared functionality used by other services
  • Handle cross-cutting concerns (authorization, validation, notifications)
  • Don't directly map to a domain entity

Examples: AuthenticationService, AuthorizationService, EmailService, NotificationService

Service Categories

1. External API Services

Services that communicate with external systems.

import { env } from "@/​env";
import { ServiceUnavailableError, UnauthenticatedError } from "@/​errors";
import { responseSchema, type ResponseType } from "@/​schemas/​response.schema";

export class ExternalApiService {
  private readonly baseUrl: string;

  constructor() {
    this.baseUrl = env.EXTERNAL_SERVICE_URL;

    if (!this.baseUrl) {
      throw new ServiceUnavailableError(
        "External service is not properly configured.",
      );
    }
  }

  async fetchData(token: string): Promise<ResponseType> {
    try {
      const response = await fetch(`${this.baseUrl}/​endpoint`, {
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/​json",
        },
      });

      if (!response.ok) {
        this.handleHttpError(response.status);
      }

      const rawData = await response.json();
      return this.validateResponse(rawData);
    } catch (error) {
      this.handleError(error);
    }
  }

  private handleHttpError(status: number): never {
    if (status === 401 || status === 403) {
      throw new UnauthenticatedError("Invalid authentication token");
    }
    throw new ServiceUnavailableError(`External service error: ${status}`);
  }

  private validateResponse(data: unknown): ResponseType {
    const parsed = responseSchema.safeParse(data);
    if (!parsed.success) {
      console.error("Invalid response format:", parsed.error.format());
      throw new ServiceUnavailableError("Invalid response format");
    }
    return parsed.data;
  }

  private handleError(error: unknown): never {
    // Re-throw known domain errors
    if (
      error instanceof UnauthenticatedError ||
      error instanceof ServiceUnavailableError
    ) {
      throw error;
    }

    console.error("External service error:", error);
    throw new ServiceUnavailableError("External service unavailable");
  }
}

Key patterns:

  • Read config from @/​env (never process.env directly)
  • Validate responses with Zod schemas
  • Throw domain errors from @/​errors
  • Handle and wrap unknown errors

2. Authorization Services

Services that provide permission logic.

import type { AuthenticatedUserContextType } from "@/​schemas/​user.schemas";
import type { {Entity}Type } from "@/​schemas/​{entity}.schema";

export class AuthorizationService {
  isAdmin(user: AuthenticatedUserContextType): boolean {
    return user.globalRole === "admin";
  }

  // --- {Entity} Permissions ---

  async canView{Entity}(
    user: AuthenticatedUserContextType,
    {entity}: {Entity}Type,
  ): Promise<boolean> {
    if (this.isAdmin(user)) return true;
    if ({entity}.createdBy === user.userId) return true;
    return false;
  }

  async canCreate{Entity}(user: AuthenticatedUserContextType): Promise<boolean> {
    if (this.isAdmin(user)) return true;
    if (user.globalRole === "user") return true;
    return false;
  }

  async canUpdate{Entity}(
    user: AuthenticatedUserContextType,
    {entity}: {Entity}Type,
  ): Promise<boolean> {
    if (this.isAdmin(user)) return true;
    if ({entity}.createdBy === user.userId) return true;
    return false;
  }

  async canDelete{Entity}(
    user: AuthenticatedUserContextType,
    {entity}: {Entity}Type,
  ): Promise<boolean> {
    if (this.isAdmin(user)) return true;
    if ({entity}.createdBy === user.userId) return true;
    return false;
  }

  // --- Event Permissions ---

  async canReceive{Entity}Event(
    user: AuthenticatedUserContextType,
    {entity}Data: { createdBy: string; [key: string]: unknown },
  ): Promise<boolean> {
    // Apply same rules as viewing
    if (this.isAdmin(user)) return true;
    if ({entity}Data.createdBy === user.userId) return true;
    return false;
  }
}

Key patterns:

  • Methods are async for consistency (even if currently sync)
  • Return boolean not throw errors (let caller decide)
  • Admin check is a shared helper
  • Group permissions by entity with comments

3. Notification/​Communication Services

Services that send notifications, emails, or messages.

import { env } from "@/​env";
import { ServiceUnavailableError } from "@/​errors";

export interface EmailOptions {
  to: string;
  subject: string;
  body: string;
  html?: boolean;
}

export class EmailService {
  private readonly apiKey: string;
  private readonly fromAddress: string;

  constructor() {
    this.apiKey = env.EMAIL_API_KEY;
    this.fromAddress = env.EMAIL_FROM_ADDRESS;

    if (!this.apiKey || !this.fromAddress) {
      throw new ServiceUnavailableError(
        "Email service is not properly configured.",
      );
    }
  }

  async send(options: EmailOptions): Promise<boolean> {
    try {
      // External API call implementation
      return true;
    } catch (error) {
      console.error("Email service error:", error);
      throw new ServiceUnavailableError("Email service unavailable");
    }
  }
}

Patterns & Rules

No BaseService Extension

Utility services are standalone classes - don't extend BaseService:

// Correct
export class AuthenticationService {
  // ...
}

// Wrong - BaseService is for resource services
export class AuthenticationService extends BaseService {
  // ...
}

No Repository Injection

Utility services don't directly access data:

// Correct - calls external API or provides logic
export class AuthenticationService {
  async authenticate(token: string) {
    return fetch(`${this.authUrl}/​auth/​me`, ...);
  }
}

// Wrong - use resource service for data access
export class AuthenticationService {
  constructor(private userRepository: IUserRepository) {}
}

Multiple Implementations (Provider Pattern)

When you need to support multiple providers (e.g., different email services, notification channels, or payment gateways), create an interface and provide multiple implementations:

// Interface in src/​services/​email.service.ts
export interface IEmailService {
  send(options: EmailOptions): Promise<EmailResult>;
  sendTemplate(to: string, templateId: string, variables: Record<string, string>): Promise<EmailResult>;
}

// SendGrid implementation in src/​services/​sendgrid-email.service.ts
export class SendGridEmailService implements IEmailService {
  async send(options: EmailOptions): Promise<EmailResult> {
    // SendGrid-specific implementation
  }
  async sendTemplate(...): Promise<EmailResult> {
    // SendGrid-specific implementation
  }
}

// Mailgun implementation in src/​services/​mailgun-email.service.ts
export class MailgunEmailService implements IEmailService {
  async send(options: EmailOptions): Promise<EmailResult> {
    // Mailgun-specific implementation
  }
  async sendTemplate(...): Promise<EmailResult> {
    // Mailgun-specific implementation
  }
}

Then inject the interface in dependent services:

export class NotificationService {
  constructor(private emailService: IEmailService) {}

  async notifyUser(userId: string, message: string) {
    await this.emailService.send({
      to: userEmail,
      subject: "Notification",
      body: message,
    });
  }
}

// Usage - choose provider based on config
const emailService =
  env.EMAIL_PROVIDER === "sendgrid"
    ? new SendGridEmailService()
    : new MailgunEmailService();

const notificationService = new NotificationService(emailService);

When to use this pattern:

  • Multiple email providers (SendGrid, Mailgun, SES)
  • Multiple notification channels (email, SMS, push)
  • Multiple payment gateways (Stripe, PayPal)
  • Multiple storage backends (S3, GCS, local)

Error Handling

Use domain errors from @/​errors:

import {
  ServiceUnavailableError,
  UnauthenticatedError,
  UnauthorizedError,
} from "@/​errors";

// Throw appropriate domain errors
if (!response.ok) {
  throw new ServiceUnavailableError("Service unavailable");
}

// Re-throw known errors, wrap unknown ones
if (error instanceof ServiceUnavailableError) {
  throw error;
}
throw new ServiceUnavailableError("Unknown error");

Configuration

Always read from validated env:

import { env } from "@/​env";

// Correct
const apiUrl = env.API_URL;

// Wrong - bypasses validation
const apiUrl = process.env.API_URL;

Response Validation

Always validate external data with Zod:

const rawData = await response.json();
const parsed = schema.safeParse(rawData);

if (!parsed.success) {
  console.error("Invalid format:", parsed.error.format());
  throw new ServiceUnavailableError("Invalid response format");
}

return parsed.data;

Complete Examples

See REFERENCE.md for complete examples:

  • AuthenticationService - External API integration
  • AuthorizationService - Permission logic

What NOT to Do

  • Do NOT extend BaseService (that's for resource services)
  • Do NOT inject repositories (use resource services for data access)
  • Do NOT use process.env directly (use @/​env)
  • Do NOT return HTTP status codes (use domain errors)
  • Do NOT swallow errors silently (log and re-throw)

See Also

  • create-service - Guide for choosing service type
  • create-resource-service - CRUD services for domain entities
  • add-env-variable - Adding environment variables for service configuration

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View