npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@200systems/mf-service-base

v1.1.7

Published

Service layer base classes with validation, error handling, and business logic patterns

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-base

Dependencies

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:

  1. 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
  });
}
  1. 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);
  1. 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 code

2. 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