
Security News
Software Engineering Daily Podcast: Feross on AI, Open Source, and Supply Chain Risk
Socket CEO Feross Aboukhadijeh joins Software Engineering Daily to discuss modern software supply chain attacks and rising AI-driven security risks.
@groundbrick/service-base
Advanced tools
Service layer base classes with validation, error handling, business logic patterns, and generic email infrastructure
🧠 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.
BusinessError)npm install @groundbrick/service-base
This package requires:
@groundbrick/logger - Logging functionality@groundbrick/db-core - Database interfacesimport { BaseService, BusinessError, ValidationHelper } from '@groundbrick/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();
}
}
}
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;
});
}
}
// 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
});
}
// 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);
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;
});
}
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
}
}
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...
}
}
The abstract base class that your services should extend.
interface ServiceOptions {
database?: DatabaseClient; // For transaction support
validator?: Validator; // Custom validator instance
config?: Record<string, any>; // Service-specific config
}
// 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
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
}
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
}
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'] }
}
// 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')!]
};
const rules = {
email: [ValidationHelper.required(), ValidationHelper.email()],
password: ValidationHelper.when(
(data) => data.isNewUser === true,
ValidationHelper.combine(
ValidationHelper.required(),
ValidationHelper.minLength(8)
)
)
};
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
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;
}
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);
}
}
}
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;
});
}
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;
});
}
// ✅ 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
}
// ✅ 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');
}
// ...
}
// ✅ 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 });
}
// ✅ 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);
}
}
This service layer integrates seamlessly with other microframework packages:
import { createLogger } from '@groundbrick/logger';
import { DatabaseFactory } from '@groundbrick/db-postgres';
import { UserRepository } from '@groundbrick/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: 'john@example.com'
});
MIT
FAQs
Service layer base classes with validation, error handling, business logic patterns, and generic email infrastructure
We found that @groundbrick/service-base demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh joins Software Engineering Daily to discuss modern software supply chain attacks and rising AI-driven security risks.

Security News
GitHub has revoked npm classic tokens for publishing; maintainers must migrate, but OpenJS warns OIDC trusted publishing still has risky gaps for critical projects.

Security News
Rust’s crates.io team is advancing an RFC to add a Security tab that surfaces RustSec vulnerability and unsoundness advisories directly on crate pages.