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.
$ 安裝
git clone https://github.com/madooei/backend-template /tmp/backend-template && cp -r /tmp/backend-template/.claude/skills/create-utility-service ~/.claude/skills/backend-template// tip: Run this command in your terminal to install the skill
name: create-utility-service description: 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.
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
Repository
