create-utility-service
About
This skill creates utility services for cross-cutting concerns like authentication, email, or external API integrations. Unlike domain services, these don't extend BaseService or inject repositories, focusing instead on shared functionality. Use it when building services that provide specialized capabilities used across multiple parts of your application.
Quick Install
Claude Code
Recommended/plugin add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/create-utility-serviceCopy and paste this command in Claude Code to install this skill
Documentation
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(neverprocess.envdirectly) - 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
asyncfor consistency (even if currently sync) - Return
booleannot 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 integrationAuthorizationService- 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.envdirectly (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 typecreate-resource-service- CRUD services for domain entitiesadd-env-variable- Adding environment variables for service configuration
GitHub Repository
Related Skills
sglang
MetaSGLang is a high-performance LLM serving framework that specializes in fast, structured generation for JSON, regex, and agentic workflows using its RadixAttention prefix caching. It delivers significantly faster inference, especially for tasks with repeated prefixes, making it ideal for complex, structured outputs and multi-turn conversations. Choose SGLang over alternatives like vLLM when you need constrained decoding or are building applications with extensive prefix sharing.
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
llamaguard
OtherLlamaGuard is Meta's 7-8B parameter model for moderating LLM inputs and outputs across six safety categories like violence and hate speech. It offers 94-95% accuracy and can be deployed using vLLM, Hugging Face, or Amazon SageMaker. Use this skill to easily integrate content filtering and safety guardrails into your AI applications.
