service-patterns
Service layer patterns for Ballee using BaseService, Result types, mappers, storage operations, soft delete handling, and proper error handling. Use when creating services, implementing CRUD operation
Files1
View on GitHubSKILL.mdtemplates/skills/service-patterns
GitHubService Patterns
Service layer patterns for clean architecture with proper error handling, logging, and type safety.
Template Usage: Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution.
Result Type Pattern
Never throw exceptions from services. Always return a Result type.
// lib/result.ts
export type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
export function ok<T>(data: T): Result<T, never> {
return { success: true, data };
}
export function err<E>(error: E): Result<never, E> {
return { success: false, error };
}
Base Service Pattern
// lib/base-service.ts
import { Logger } from './logger';
export abstract class BaseService {
protected logger: Logger;
constructor(serviceName: string) {
this.logger = new Logger(serviceName);
}
protected handleError<T>(
operation: string,
error: unknown,
context?: Record<string, unknown>
): Result<T, ServiceError> {
this.logger.error(`${operation} failed`, { error, ...context });
if (error instanceof ServiceError) {
return err(error);
}
return err(new ServiceError(
error instanceof Error ? error.message : 'Unknown error',
'INTERNAL_ERROR'
));
}
}
export class ServiceError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
}
}
Service Implementation
// services/user.service.ts
import { BaseService, ServiceError } from '@/lib/base-service';
import { ok, err, Result } from '@/lib/result';
import { db } from '@/lib/db';
import { User, CreateUserInput, UpdateUserInput } from '@/types';
export class UserService extends BaseService {
constructor() {
super('UserService');
}
async findById(id: string): Promise<Result<User | null, ServiceError>> {
try {
const user = await db.user.findUnique({ where: { id } });
return ok(user);
} catch (error) {
return this.handleError('findById', error, { id });
}
}
async create(input: CreateUserInput): Promise<Result<User, ServiceError>> {
try {
// Validate business rules
const existing = await db.user.findUnique({
where: { email: input.email }
});
if (existing) {
return err(new ServiceError(
'Email already in use',
'EMAIL_EXISTS',
409
));
}
const user = await db.user.create({ data: input });
this.logger.info('User created', { userId: user.id });
return ok(user);
} catch (error) {
return this.handleError('create', error, { email: input.email });
}
}
async update(
id: string,
input: UpdateUserInput
): Promise<Result<User, ServiceError>> {
try {
const user = await db.user.update({
where: { id },
data: input,
});
this.logger.info('User updated', { userId: id });
return ok(user);
} catch (error) {
return this.handleError('update', error, { id });
}
}
async delete(id: string): Promise<Result<void, ServiceError>> {
try {
await db.user.delete({ where: { id } });
this.logger.info('User deleted', { userId: id });
return ok(undefined);
} catch (error) {
return this.handleError('delete', error, { id });
}
}
}
// Singleton instance
export const userService = new UserService();
Mapper Pattern
// mappers/user.mapper.ts
import { User as DbUser } from '@prisma/client';
import { User } from '@/types';
export class UserMapper {
static toEntity(dbUser: DbUser): User {
return {
id: dbUser.id,
email: dbUser.email,
name: dbUser.name,
createdAt: dbUser.created_at,
updatedAt: dbUser.updated_at,
};
}
static toEntities(dbUsers: DbUser[]): User[] {
return dbUsers.map(this.toEntity);
}
}
Transaction Pattern
async createUserWithProfile(
userData: CreateUserInput,
profileData: CreateProfileInput
): Promise<Result<{ user: User; profile: Profile }, ServiceError>> {
try {
const result = await db.$transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
const profile = await tx.profile.create({
data: { ...profileData, userId: user.id }
});
return { user, profile };
});
return ok(result);
} catch (error) {
return this.handleError('createUserWithProfile', error);
}
}
Batch Operations (Avoid N+1)
// BAD: N+1 query
async getUsersWithPosts(userIds: string[]) {
const users = await db.user.findMany({ where: { id: { in: userIds } } });
// N+1: One query per user!
for (const user of users) {
user.posts = await db.post.findMany({ where: { userId: user.id } });
}
}
// GOOD: Batch query
async getUsersWithPosts(userIds: string[]) {
const [users, posts] = await Promise.all([
db.user.findMany({ where: { id: { in: userIds } } }),
db.post.findMany({ where: { userId: { in: userIds } } }),
]);
// Join in memory
const postsMap = new Map<string, Post[]>();
for (const post of posts) {
const userPosts = postsMap.get(post.userId) || [];
userPosts.push(post);
postsMap.set(post.userId, userPosts);
}
return users.map(user => ({
...user,
posts: postsMap.get(user.id) || [],
}));
}
Service Usage in Actions
'use server';
import { userService } from '@/services/user.service';
import { revalidatePath } from 'next/cache';
export async function createUserAction(formData: FormData) {
const result = await userService.create({
email: formData.get('email') as string,
name: formData.get('name') as string,
});
if (!result.success) {
return {
success: false,
error: result.error.message,
code: result.error.code,
};
}
revalidatePath('/users');
return { success: true, data: result.data };
}
Checklist
- Returns Result<T, E> - never throws
- Logs errors with context
- Uses transactions for multi-step operations
- Avoids N+1 queries
- Maps DB entities to domain types
- Validates business rules
- Handles edge cases