@200systems/mf-service-base
v1.1.7
Published
Service layer base classes with validation, error handling, and business logic patterns
Maintainers
Readme
@200systems/mf-service-base
🧠 Service layer foundation with validation, logging, error handling, and transaction support.
Service layer base classes and utilities for business logic implementation in layered architecture applications.
🚀 Features
🎯 BaseService Class
- Integrated logging with service context
- Database transaction management
- Input validation framework
- Standardized error handling
- Repository integration patterns
🔧 Business Logic Patterns
- Input validation before repository calls
- Business rule enforcement
- Cross-entity operations coordination
- Response transformation and mapping
⚡ Error Handling
- Business-specific error types (
BusinessError) - Validation error aggregation
- Repository error transformation
- Structured error responses for APIs
✅ Validation System
- Simple, extensible validation framework
- Common validation rules included
- Custom rule registration
- Field-level error reporting
Installation
npm install @200systems/mf-service-baseDependencies
This package requires:
@200systems/mf-logger- Logging functionality@200systems/mf-db-core- Database interfaces
Quick Start
1. Basic Service Implementation
import { BaseService, BusinessError, ValidationHelper } from '@200systems/mf-service-base';
import { UserRepository } from './UserRepository';
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
interface CreateUserRequest {
name: string;
email: string;
}
export class UserService extends BaseService {
constructor(private userRepository: UserRepository) {
super();
}
async createUser(userData: CreateUserRequest): Promise<User> {
const endTimer = this.startOperation('createUser');
try {
// 1. Validate input
const validationResult = await this.validate(userData, {
name: [
ValidationHelper.required(),
ValidationHelper.minLength(2),
ValidationHelper.maxLength(100)
],
email: [
ValidationHelper.required(),
ValidationHelper.email()
]
});
if (!validationResult.isValid) {
throw new BusinessError(
'Invalid user data',
'VALIDATION_FAILED',
undefined,
{ errors: validationResult.errors }
);
}
// 2. Business logic
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new BusinessError(
'User already exists',
'USER_EXISTS',
undefined,
{ email: userData.email }
);
}
// 3. Create user
const user = await this.userRepository.create({
name: userData.name,
email: userData.email.toLowerCase(),
active: true
});
this.logger.info('User created successfully', { userId: user.id });
return user;
} catch (error) {
if (BusinessError.isBusinessError(error)) {
throw error;
}
throw this.handleRepositoryError(error as Error);
} finally {
endTimer();
}
}
}2. Using Transactions
export class OrderService extends BaseService {
constructor(
private orderRepository: OrderRepository,
private inventoryRepository: InventoryRepository,
options?: ServiceOptions
) {
super(options);
}
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
// Use transaction for multi-repository operations
return await this.withTransaction(async (tx) => {
// Create order
const order = await this.orderRepository.createWithTransaction(tx, {
user_id: orderData.user_id,
total_amount: orderData.total
});
// Update inventory
for (const item of orderData.items) {
await this.inventoryRepository.decrementStockWithTransaction(
tx,
item.product_id,
item.quantity
);
}
return order;
});
}
}📋 Transaction pattern:
- Create Parent First, Use Generated ID
// Inside withTransaction callback
const newOrder = await this.orderRepository.createWithTransaction(tx, {
user_id: orderData.user_id,
total_amount: totalAmount,
status: 'pending'
});
// newOrder.id is now available (auto-generated by database)
// Use the order ID for child records
for (const item of orderData.items) {
await this.orderItemRepository.createWithTransaction(tx, {
order_id: newOrder.id, // 👈 Use the generated ID
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price
});
}- Alternative: Bulk Creation
// Create all order items at once
const orderItemsData = orderData.items.map(item => ({
order_id: newOrder.id, // Same ID for all items
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price
}));
await this.orderItemRepository.createManyWithTransaction(tx, orderItemsData);- Complete Example with Proper Flow
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
return await this.withTransaction(async (tx) => {
// 1. Validate business rules first
await this.validateOrderData(orderData);
// 2. Calculate total amount
let totalAmount = 0;
const validatedItems = [];
for (const item of orderData.items) {
const product = await this.productRepository.findByIdWithTransaction(tx, item.product_id);
// ... validation logic
totalAmount += product.price * item.quantity;
validatedItems.push({
product_id: item.product_id,
quantity: item.quantity,
unit_price: product.price
});
}
// 3. Create the order (gets auto-generated ID)
const newOrder = await this.orderRepository.createWithTransaction(tx, {
user_id: orderData.user_id,
total_amount: totalAmount,
status: 'pending',
created_at: new Date()
});
// 4. Create order items using the order ID
for (const item of validatedItems) {
await this.orderItemRepository.createWithTransaction(tx, {
order_id: newOrder.id, // 👈 Key: Use generated order ID
product_id: item.product_id,
quantity: item.quantity,
unit_price: item.unit_price
});
}
// 5. Update product inventory
for (const item of orderData.items) {
await this.productRepository.decrementStockWithTransaction(
tx,
item.product_id,
item.quantity
);
}
// 6. Return the complete order (with ID)
return newOrder;
});
}🔧 Repository Method Requirements:
Your repositories need to support transaction-aware methods:
// In your OrderRepository
class OrderRepository extends BaseRepository<Order> {
async createWithTransaction(tx: DatabaseTransaction, data: Partial<Order>): Promise<Order> {
// Use tx.query() instead of this.db.query()
const result = await tx.query(
'INSERT INTO orders (user_id, total_amount, status, created_at) VALUES (?, ?, ?, ?) RETURNING *',
[data.user_id, data.total_amount, data.status, data.created_at]
);
return result.rows[0];
}
}
// In your OrderItemRepository
class OrderItemRepository extends BaseRepository<OrderItem> {
async createWithTransaction(tx: DatabaseTransaction, data: Partial<OrderItem>): Promise<OrderItem> {
const result = await tx.query(
'INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?) RETURNING *',
[data.order_id, data.product_id, data.quantity, data.unit_price]
);
return result.rows[0];
}
async createManyWithTransaction(tx: DatabaseTransaction, items: Partial<OrderItem>[]): Promise<OrderItem[]> {
// Bulk insert implementation
const values = items.map(item => [item.order_id, item.product_id, item.quantity, item.unit_price]);
// ... bulk insert logic
}
}💡 Key Points:
- Order matters: Create parent record first to get the ID
- Use the transaction: All operations must use the same tx parameter
- ID is immediately available: After createWithTransaction, the returned object has the generated ID
- All-or-nothing: If any step fails, the entire transaction rolls back
- Performance: Consider bulk operations for multiple child records
3. Custom Validation Rules
export class ProductService extends BaseService {
constructor(private productRepository: ProductRepository) {
super();
// Register custom validation rules
this.validator.registerRule('sku', (value) => {
const skuPattern = /^[A-Z]{2}-\d{4}$/;
return skuPattern.test(value) ? null : 'SKU must follow format: XX-0000';
});
}
async createProduct(productData: CreateProductRequest): Promise<Product> {
const validationResult = await this.validate(productData, {
name: [ValidationHelper.required(), ValidationHelper.minLength(2)],
sku: [ValidationHelper.required(), this.validator.getRule('sku')!],
price: [ValidationHelper.required(), ValidationHelper.numberRange(0.01, 10000)]
});
if (!validationResult.isValid) {
throw new BusinessError('Invalid product data', 'VALIDATION_FAILED', undefined, {
errors: validationResult.errors
});
}
// Create product logic...
}
}API Reference
BaseService
The abstract base class that your services should extend.
Constructor Options
interface ServiceOptions {
database?: DatabaseClient; // For transaction support
validator?: Validator; // Custom validator instance
config?: Record<string, any>; // Service-specific config
}Protected Methods
// Validation
protected async validate<T>(data: T, rules: ValidationRules): Promise<ValidationResult>
protected validateRequired(data: Record<string, any>, fields: string[]): void
// Transactions
protected async withTransaction<T>(callback: (tx: DatabaseTransaction) => Promise<T>): Promise<T>
// Error Handling
protected handleRepositoryError(error: Error): BusinessError
// Utilities
protected startOperation(operation: string, metadata?: Record<string, any>): () => void
protected safeSerialize(obj: any): any🛡 BusinessError
Custom error class for business logic errors.
class BusinessError extends Error {
constructor(
message: string,
code: string,
originalError?: Error,
context?: Record<string, any>
)
// Properties
readonly code: string
readonly originalError?: Error
readonly context?: Record<string, any>
readonly timestamp: Date
// Methods
toJSON(): Record<string, any>
toUserResponse(): { error: string; code: string; timestamp: string }
hasCode(code: string): boolean
static isBusinessError(error: any): error is BusinessError
}ValidationHelper
Pre-built validation rules for common scenarios.
class ValidationHelper {
// Basic rules
static required(): ValidationRule
static minLength(min: number): ValidationRule
static maxLength(max: number): ValidationRule
static email(): ValidationRule
static numberRange(min?: number, max?: number): ValidationRule
static pattern(regex: RegExp, message: string): ValidationRule
static oneOf(options: any[], message?: string): ValidationRule
// Array rules
static arrayMinLength(min: number): ValidationRule
// Advanced rules
static custom(validator: (value: any, data?: any) => string | null): ValidationRule
static when(condition: (data: any) => boolean, rule: ValidationRule): ValidationRule
// Utility methods
static combine(...rules: ValidationRule[]): ValidationRule[]
static entityId(): ValidationRule[]
// Pre-configured rule sets
static userCreation(): ValidationRules
static pagination(): ValidationRules
}✅ Validation System
Basic Validation
const result = await this.validate(data, {
email: [ValidationHelper.required(), ValidationHelper.email()],
age: [ValidationHelper.numberRange(18, 120)]
});
if (!result.isValid) {
// Handle validation errors
console.log(result.errors); // { email: ['Must be a valid email'], age: ['Must be at least 18'] }
}🧠 Custom Validation Rules
// Register a custom rule
this.validator.registerRule('phone', (value) => {
const phonePattern = /^\+?[\d\s-()]{10,}$/;
return phonePattern.test(value) ? null : 'Invalid phone number format';
});
// Use in validation
const rules = {
phone: [ValidationHelper.required(), this.validator.getRule('phone')!]
};Conditional Validation
const rules = {
email: [ValidationHelper.required(), ValidationHelper.email()],
password: ValidationHelper.when(
(data) => data.isNewUser === true,
ValidationHelper.combine(
ValidationHelper.required(),
ValidationHelper.minLength(8)
)
)
};Error Handling Patterns
1. Repository Error Transformation
The handleRepositoryError method automatically transforms common database errors:
// Database constraint violation → BusinessError with DUPLICATE_RESOURCE code
// Record not found → BusinessError with RESOURCE_NOT_FOUND code
// Foreign key violation → BusinessError with CONSTRAINT_VIOLATION code
// Other errors → BusinessError with OPERATION_FAILED code2. Structured Error Responses
try {
const user = await userService.createUser(userData);
return createSuccessResult(user);
} catch (error) {
if (BusinessError.isBusinessError(error)) {
// Handle business logic errors
return createErrorResult(error);
}
// Handle unexpected errors
throw error;
}3. API Integration
export class UserController {
async createUser(req: any, res: any) {
try {
const user = await this.userService.createUser(req.body);
res.status(201).json(createSuccessResult(user));
} catch (error) {
const result = createErrorResult(error as Error);
// Map business error codes to HTTP status codes
let status = 500;
if (BusinessError.isBusinessError(error)) {
switch (error.code) {
case 'VALIDATION_FAILED': status = 400; break;
case 'USER_EXISTS': status = 409; break;
case 'USER_NOT_FOUND': status = 404; break;
}
}
res.status(status).json(result);
}
}
}Transaction Management
Simple Transaction
async updateUserProfile(userId: number, profileData: any): Promise<User> {
return await this.withTransaction(async (tx) => {
// Update user
const user = await this.userRepository.updateWithTransaction(tx, userId, {
name: profileData.name,
email: profileData.email
});
// Update user preferences
await this.preferencesRepository.updateWithTransaction(tx, userId, {
theme: profileData.theme,
language: profileData.language
});
return user;
});
}Complex Business Transaction
async processOrder(orderData: CreateOrderRequest): Promise<Order> {
return await this.withTransaction(async (tx) => {
// 1. Validate inventory
for (const item of orderData.items) {
const product = await this.productRepository.findByIdWithTransaction(tx, item.productId);
if (product.stock < item.quantity) {
throw new BusinessError('Insufficient stock', 'INSUFFICIENT_STOCK');
}
}
// 2. Create order
const order = await this.orderRepository.createWithTransaction(tx, orderData);
// 3. Update inventory
for (const item of orderData.items) {
await this.productRepository.decrementStockWithTransaction(
tx, item.productId, item.quantity
);
}
// 4. Send notification (example of non-transactional side effect)
// Note: This should be handled outside the transaction
// Consider using event-driven patterns for side effects
return order;
});
}Best Practices
1. Keep Services Focused
// ✅ Good - Single responsibility
class UserService extends BaseService {
async createUser(userData: CreateUserRequest): Promise<User> { }
async updateUser(id: number, updates: UpdateUserRequest): Promise<User> { }
async getUserById(id: number): Promise<User> { }
}
// ❌ Bad - Too many responsibilities
class UserService extends BaseService {
async createUser(userData: CreateUserRequest): Promise<User> { }
async sendWelcomeEmail(user: User): Promise<void> { } // Should be EmailService
async generateReport(userId: number): Promise<Report> { } // Should be ReportService
}2. Validate Input Early
// ✅ Good - Validate first
async createUser(userData: CreateUserRequest): Promise<User> {
const validationResult = await this.validate(userData, this.getUserValidationRules());
if (!validationResult.isValid) {
throw new BusinessError('Validation failed', 'VALIDATION_FAILED', undefined, {
errors: validationResult.errors
});
}
// Continue with business logic...
}
// ❌ Bad - Validation mixed with business logic
async createUser(userData: CreateUserRequest): Promise<User> {
const existingUser = await this.userRepository.findByEmail(userData.email);
if (!userData.email) { // Too late for basic validation
throw new Error('Email required');
}
// ...
}3. Use Transactions for Multi-Repository Operations
// ✅ Good - Transaction for consistency
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
await this.withTransaction(async (tx) => {
await this.accountRepository.decrementBalanceWithTransaction(tx, fromAccount, amount);
await this.accountRepository.incrementBalanceWithTransaction(tx, toAccount, amount);
await this.transactionRepository.createWithTransaction(tx, {
from_account: fromAccount,
to_account: toAccount,
amount
});
});
}
// ❌ Bad - No transaction, inconsistent state possible
async transferFunds(fromAccount: number, toAccount: number, amount: number): Promise<void> {
await this.accountRepository.decrementBalance(fromAccount, amount);
await this.accountRepository.incrementBalance(toAccount, amount); // Could fail, leaving inconsistent state
await this.transactionRepository.create({ from_account: fromAccount, to_account: toAccount, amount });
}4. Handle Errors Appropriately
// ✅ Good - Structured error handling
async getUserById(id: number): Promise<User> {
try {
this.validateRequired({ id }, ['id']);
const user = await this.userRepository.findById(id);
if (!user) {
throw new BusinessError('User not found', 'USER_NOT_FOUND', undefined, { userId: id });
}
return user;
} catch (error) {
if (BusinessError.isBusinessError(error)) {
throw error; // Re-throw business errors
}
// Transform repository errors
throw this.handleRepositoryError(error as Error);
}
}Integration with Other Packages
This service layer integrates seamlessly with other microframework packages:
import { createLogger } from '@200systems/mf-logger';
import { DatabaseFactory } from '@200systems/mf-db-postgres';
import { UserRepository } from '@200systems/mf-repository-base';
import { UserService } from './UserService';
// Initialize database
const db = DatabaseFactory.getInstance({
host: 'localhost',
database: 'myapp',
user: 'user',
password: 'password'
});
// Initialize repository
const userRepository = new UserRepository(db);
// Initialize service with database for transaction support
const userService = new UserService(userRepository, { database: db });
// Use in application
const user = await userService.createUser({
name: 'John Doe',
email: '[email protected]'
});Testing
The package includes comprehensive test utilities. See the src/__tests__/ directory for examples of:
- Service unit testing with mocked dependencies
- Validation rule testing
- Error handling testing
- Transaction behavior testing
📜 License
MIT
