@rvettori/service-pattern
v1.0.1
Published
Service pattern implementation with Result type for TypeScript - Single Responsibility Principle
Maintainers
Readme
@rvettori/service-pattern
Service pattern implementation with Result type for TypeScript. Implements Single Responsibility Principle with type-safe error handling.
🚀 Features
- ✅ Result Pattern: Handle errors without exceptions
- ✅ Service Interface: Single responsibility pattern for services
- ✅ Type-Safe: Full TypeScript support with generics
- ✅ Zero Dependencies: Lightweight and fast
- ✅ Functional Helpers: map, flatMap, unwrap, combine, and more
- ✅ Error Types: Structured error handling with ErrorDetail
📦 Installation
npm install @rvettori/service-pattern
# or
bun add @rvettori/service-pattern🎯 Quick Start
Basic Service
import { IService, Result, Ok, Err, ErrorDetail } from '@rvettori/service-pattern';
interface CreateUserInput {
email: string;
password: string;
}
interface User {
id: number;
email: string;
}
class CreateUserService implements IService<CreateUserInput, User, ErrorDetail> {
async execute(input: CreateUserInput): Promise<Result<User, ErrorDetail>> {
// Validate email
if (!input.email.includes('@')) {
return Err({
code: 'INVALID_EMAIL',
message: 'Email must be valid'
});
}
// Create user
const user = { id: 1, email: input.email };
return Ok(user);
}
}Using the Service
const service = new CreateUserService();
const result = await service.execute({
email: '[email protected]',
password: 'secret'
});
if (result.success) {
console.log('User created:', result.value);
} else {
console.error('Error:', result.error.message);
}📚 API Reference
Result Pattern
Result<T, E>
Type representing success or failure:
type Result<T, E> =
| { success: true; value: T }
| { success: false; error: E };Ok(value)
Creates a successful Result:
const result = Ok({ id: 1, name: 'John' });
// { success: true, value: { id: 1, name: 'John' } }Err(error)
Creates an error Result:
const result = Err('User not found');
// { success: false, error: 'User not found' }isOk(result) / isErr(result)
Type guards for checking Result state:
if (isOk(result)) {
console.log(result.value); // TypeScript knows it's success
}
if (isErr(result)) {
console.log(result.error); // TypeScript knows it's error
}map(result, fn)
Transform the value of a successful Result:
const result = Ok(5);
const doubled = map(result, x => x * 2); // Ok(10)flatMap(result, fn)
Chain operations that return Results:
const divide = (x: number) =>
x !== 0 ? Ok(10 / x) : Err('Division by zero');
const result = Ok(2);
const divided = flatMap(result, divide); // Ok(5)unwrap(result)
Extract value or throw:
const value = unwrap(Ok(42)); // 42
unwrap(Err('error')); // throws ErrorunwrapOr(result, defaultValue)
Extract value or return default:
unwrapOr(Ok(42), 0); // 42
unwrapOr(Err('error'), 0); // 0combine(results)
Combine multiple Results into one:
const results = [Ok(1), Ok(2), Ok(3)];
combine(results); // Ok([1, 2, 3])
const withError = [Ok(1), Err('error'), Ok(3)];
combine(withError); // Err('error')Service Interfaces
IService<Input, Output, Error>
Base interface for all services:
interface IService<Input, Output, Error = string> {
execute(input: Input): Promise<Result<Output, Error>>;
}IServiceNoInput<Output, Error>
For services without input parameters:
class ListUsersService implements IServiceNoInput<User[], ErrorDetail> {
async execute(): Promise<Result<User[], ErrorDetail>> {
// implementation
}
}IServiceSync<Input, Output, Error>
For synchronous services:
class ValidateEmailService implements IServiceSync<string, boolean, ErrorDetail> {
execute(email: string): Result<boolean, ErrorDetail> {
// synchronous validation
}
}Error Types
ErrorDetail<Code>
Structured error with code and message:
interface ErrorDetail<Code extends string = string> {
code: Code;
message: string;
details?: any;
}createError(code, message, details?)
Factory function for creating errors:
const error = createError('USER_NOT_FOUND', 'User not found', { id: 123 });🏗️ Architecture Example
Domain Layer
// services/domain/HashPasswordService.ts
import { IService, Result, Ok, Err, ErrorDetail } from '@rvettori/service-pattern';
export class HashPasswordService implements IService<string, string, ErrorDetail> {
async execute(password: string): Promise<Result<string, ErrorDetail>> {
if (password.length < 8) {
return Err({
code: 'WEAK_PASSWORD',
message: 'Password must be at least 8 characters'
});
}
const hashed = await someHashFunction(password);
return Ok(hashed);
}
}Application Layer
// services/application/RegisterUserService.ts
import { IService, Result, Ok, Err, ErrorDetail } from '@vettori/service-pattern';
export class RegisterUserService implements IService<RegisterDTO, User, ErrorDetail> {
constructor(
private hashPasswordService: HashPasswordService,
private userRepository: UserRepository
) {}
async execute(input: RegisterDTO): Promise<Result<User, ErrorDetail>> {
// Hash password using domain service
const hashResult = await this.hashPasswordService.execute(input.password);
if (!hashResult.success) {
return hashResult;
}
// Create user
const user = await this.userRepository.create({
email: input.email,
password: hashResult.value
});
return Ok(user);
}
}🧪 Testing
Services are easy to test with mocks:
describe('RegisterUserService', () => {
it('should return error if password is weak', async () => {
const mockHashService = {
execute: jest.fn().mockResolvedValue(
Err({ code: 'WEAK_PASSWORD', message: 'Too weak' })
)
};
const service = new RegisterUserService(mockHashService, mockRepo);
const result = await service.execute({ email: '[email protected]', password: '123' });
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.code).toBe('WEAK_PASSWORD');
}
});
});📖 Best Practices
- One Service, One Responsibility: Each service should do only one thing
- Use Result Pattern: Never throw exceptions, always return Result
- Type Your Errors: Use ErrorDetail with specific error codes
- Compose Services: Application services orchestrate domain services
- Test Services Independently: Mock dependencies, test logic
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
MIT © Rafael Vettori
