Best Practices
This guide outlines recommended patterns and practices for building applications with ziegel. Following these practices will help you create maintainable, scalable, and robust applications.
General Principles
1. Start Small, Scale Up
Begin with the minimal set of packages you need and add more as your requirements grow:
typescript
// Start with core functionality
import { StringExtensions, TypeGuard } from '@breadstone/ziegel-core';
import { ServiceLocator } from '@breadstone/ziegel-platform';
// Add specialized packages as needed
import { HttpClient } from '@breadstone/ziegel-platform-http';
import { LocalizationManager } from '@breadstone/ziegel-platform-localization';2. Use Dependency Injection
Always use the service locator pattern instead of direct instantiation:
typescript
// ❌ Avoid direct instantiation
class UserService {
private httpClient = new HttpClient('https://api.example.com');
private logger = new ConsoleLogger();
}
// ✅ Use dependency injection
class UserService {
private httpClient: HttpClient;
private logger: ILogger;
constructor() {
this.httpClient = ServiceLocator.get<HttpClient>('httpClient');
this.logger = ServiceLocator.get<ILogger>('logger');
}
}3. Embrace TypeScript
Leverage TypeScript's type system for better development experience:
typescript
// ✅ Use specific types
interface UserCreateRequest {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
// ✅ Use generics for reusable code
class Repository<T> {
async find<K extends keyof T>(criteria: Pick<T, K>): Promise<T[]> {
// Implementation
}
}
// ✅ Use type guards
function isUser(obj: unknown): obj is User {
return TypeGuard.isObject(obj) &&
TypeGuard.hasProperty(obj, 'id') &&
TypeGuard.hasProperty(obj, 'name');
}Package-Specific Best Practices
ziegel-core
String Handling
typescript
// ✅ Use StringExtensions for formatting
const message = StringExtensions.format('Hello, {0}! You have {1} messages.', userName, messageCount);
// ✅ Use for validation
if (StringExtensions.isNullOrWhiteSpace(userInput)) {
throw new ArgumentException('userInput', 'Input cannot be empty');
}
// ❌ Avoid string concatenation for complex scenarios
const message = `Hello, ${userName}! You have ${messageCount} messages.`; // Less flexibleException Handling
typescript
// ✅ Use specific exception types
function divide(a: number, b: number): number {
if (b === 0) {
throw new InvalidOperationException('Cannot divide by zero');
}
return a / b;
}
// ✅ Include meaningful error messages
function validateEmail(email: string): void {
if (!email) {
throw new ArgumentNullException('email', 'Email address is required');
}
if (!email.includes('@')) {
throw new ArgumentException('email', 'Email must contain @ symbol');
}
}ziegel-platform
Service Registration
typescript
// ✅ Register services at application startup
class Application {
async initialize() {
// Register core services first
ServiceLocator.register('logger', () => LoggerManager.getLogger('App'));
ServiceLocator.register('config', () => this.createConfiguration());
// Register dependent services
ServiceLocator.register('httpClient', () => this.createHttpClient());
ServiceLocator.register('userService', () => new UserService());
}
}
// ✅ Use factory functions for complex initialization
ServiceLocator.register('complexService', () => {
const config = ServiceLocator.get<ConfigurationManager>('config');
const logger = ServiceLocator.get<ILogger>('logger');
return new ComplexService(config, logger);
});Culture Management
typescript
// ✅ Setup culture provider early
const cultureProvider = new CultureProvider();
cultureProvider.setCulture('en-US'); // Set default
// ✅ Listen for culture changes
cultureProvider.cultureChanged.add((newCulture) => {
// Update UI, reload localized content, etc.
this.updateUserInterface(newCulture);
});ziegel-platform-http
HTTP Client Configuration
typescript
// ✅ Configure HTTP client with proper settings
const httpClient = new HttpClient('https://api.example.com', {
timeout: 10000,
retryAttempts: 3,
retryDelay: 1000
});
// ✅ Use interceptors for cross-cutting concerns
class LoggingInterceptor implements HttpInterceptor {
async intercept(request: Request): Promise<Request> {
console.log(`Making request to: ${request.url}`);
return request;
}
}
httpClient.addInterceptor(new LoggingInterceptor());
httpClient.addInterceptor(new AuthInterceptor());
httpClient.addInterceptor(new ErrorHandlingInterceptor());API Service Pattern
typescript
// ✅ Create dedicated API services
class UserApiService {
constructor(private httpClient: HttpClient) {}
async getUsers(page: number = 1, limit: number = 10): Promise<PaginatedResponse<User>> {
return await this.httpClient.get<PaginatedResponse<User>>('/users', {
params: { page, limit }
});
}
async createUser(userData: CreateUserRequest): Promise<User> {
return await this.httpClient.post<User>('/users', userData);
}
async updateUser(id: number, userData: UpdateUserRequest): Promise<User> {
return await this.httpClient.put<User>(`/users/${id}`, userData);
}
async deleteUser(id: number): Promise<void> {
await this.httpClient.delete(`/users/${id}`);
}
}ziegel-platform-logging
Logger Configuration
typescript
// ✅ Configure logging appropriately for environment
if (process.env.NODE_ENV === 'production') {
LoggerManager.configure({
level: LogLevel.Warning,
providers: [
new FileLogProvider('logs/app.log'),
new RemoteLogProvider(config.getValue('logging:remoteUrl'))
]
});
} else {
LoggerManager.configure({
level: LogLevel.Debug,
providers: [new ConsoleLogProvider()]
});
}Structured Logging
typescript
// ✅ Use structured logging with context
class UserService {
private logger = LoggerManager.getLogger('UserService');
async createUser(userData: CreateUserRequest): Promise<User> {
this.logger.info('Creating new user', {
email: userData.email,
role: userData.role
});
try {
const user = await this.userRepository.create(userData);
this.logger.info('User created successfully', {
userId: user.id,
email: user.email
});
return user;
} catch (error) {
this.logger.error('Failed to create user', error, {
email: userData.email
});
throw error;
}
}
}ziegel-platform-configuration
Configuration Structure
typescript
// ✅ Use hierarchical configuration structure
interface AppConfiguration {
database: {
connectionString: string;
timeout: number;
poolSize: number;
};
api: {
baseUrl: string;
timeout: number;
retryAttempts: number;
};
features: {
enableAnalytics: boolean;
enableCaching: boolean;
maxFileSize: number;
};
logging: {
level: string;
file: string;
};
}
// ✅ Validate configuration at startup
class ConfigurationValidator {
static validate(config: ConfigurationManager): void {
const requiredSettings = [
'database:connectionString',
'api:baseUrl'
];
for (const setting of requiredSettings) {
if (!config.hasValue(setting)) {
throw new Error(`Required configuration setting missing: ${setting}`);
}
}
}
}ziegel-platform-localization
Localization Setup
typescript
// ✅ Use proper key naming conventions
const translations = new Map([
['en-US', new Map([
['common.welcome', 'Welcome'],
['common.goodbye', 'Goodbye'],
['user.profile.title', 'User Profile'],
['user.profile.email', 'Email Address'],
['validation.required', 'This field is required'],
['validation.email.invalid', 'Please enter a valid email address']
])]
]);
// ✅ Handle missing translations gracefully
const localizationManager = new LocalizationManager(
provider,
cultureProvider,
(key, culture) => {
console.warn(`Missing translation: ${key} for culture: ${culture}`);
return `[${key}]`; // Fallback format
}
);Architecture Patterns
Repository Pattern
typescript
// ✅ Use repository pattern for data access
interface IUserRepository {
findById(id: number): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(userData: CreateUserRequest): Promise<User>;
update(id: number, userData: UpdateUserRequest): Promise<User>;
delete(id: number): Promise<void>;
}
class UserRepository implements IUserRepository {
constructor(
private httpClient: HttpClient,
private logger: ILogger
) {}
async findById(id: number): Promise<User | null> {
try {
return await this.httpClient.get<User>(`/users/${id}`);
} catch (error) {
if (error.status === 404) {
return null;
}
throw error;
}
}
// ... other methods
}Service Layer Pattern
typescript
// ✅ Separate business logic into services
class UserService {
constructor(
private userRepository: IUserRepository,
private emailService: IEmailService,
private logger: ILogger
) {}
async registerUser(registrationData: UserRegistrationRequest): Promise<User> {
// Validate input
this.validateRegistrationData(registrationData);
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(registrationData.email);
if (existingUser) {
throw new InvalidOperationException('User already exists with this email');
}
// Create user
const user = await this.userRepository.create({
name: registrationData.name,
email: registrationData.email,
role: 'user'
});
// Send welcome email
await this.emailService.sendWelcomeEmail(user);
this.logger.info('User registered successfully', { userId: user.id });
return user;
}
private validateRegistrationData(data: UserRegistrationRequest): void {
if (StringExtensions.isNullOrWhiteSpace(data.name)) {
throw new ArgumentException('name', 'Name is required');
}
if (StringExtensions.isNullOrWhiteSpace(data.email)) {
throw new ArgumentException('email', 'Email is required');
}
if (!data.email.includes('@')) {
throw new ArgumentException('email', 'Invalid email format');
}
}
}Event-Driven Architecture
typescript
// ✅ Use event aggregator for loose coupling
class UserEventHandler {
constructor(
private emailService: IEmailService,
private analyticsService: IAnalyticsService,
private eventAggregator: EventAggregator
) {
this.setupEventHandlers();
}
private setupEventHandlers(): void {
this.eventAggregator.subscribe('user:registered', async (user: User) => {
await this.emailService.sendWelcomeEmail(user);
this.analyticsService.trackUserRegistration(user);
});
this.eventAggregator.subscribe('user:login', async (user: User) => {
this.analyticsService.trackUserLogin(user);
});
}
}Performance Best Practices
Bundle Optimization
typescript
// ✅ Use tree-shaking friendly imports
import { StringExtensions } from '@breadstone/ziegel-core';
// ❌ Avoid importing entire packages
import * as ZiegelCore from '@breadstone/ziegel-core';Lazy Loading
typescript
// ✅ Lazy load expensive services
ServiceLocator.register('expensiveService', () => {
// Only create when first requested
return new ExpensiveService();
});
// ✅ Use dynamic imports for optional features
async function loadAnalytics() {
if (config.getValue<boolean>('features:enableAnalytics')) {
const { AnalyticsService } = await import('@breadstone/ziegel-platform-analytics');
return new AnalyticsService();
}
return null;
}Caching Strategy
typescript
// ✅ Implement appropriate caching
class UserService {
private cache = new Map<number, User>();
async getUser(id: number): Promise<User> {
// Check cache first
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
// Fetch from API
const user = await this.userRepository.findById(id);
// Cache result
if (user) {
this.cache.set(id, user);
}
return user;
}
}Testing Best Practices
Unit Testing
typescript
// ✅ Mock dependencies properly
describe('UserService', () => {
let userService: UserService;
let mockRepository: jest.Mocked<IUserRepository>;
let mockLogger: jest.Mocked<ILogger>;
beforeEach(() => {
mockRepository = jest.createMockFromModule<IUserRepository>('./IUserRepository');
mockLogger = jest.createMockFromModule<ILogger>('@breadstone/ziegel-platform-logging');
userService = new UserService(mockRepository, mockLogger);
});
it('should create user successfully', async () => {
// Arrange
const userData = { name: 'John', email: 'john@example.com' };
const expectedUser = { id: 1, ...userData };
mockRepository.create.mockResolvedValue(expectedUser);
// Act
const result = await userService.createUser(userData);
// Assert
expect(result).toEqual(expectedUser);
expect(mockRepository.create).toHaveBeenCalledWith(userData);
});
});Integration Testing
typescript
// ✅ Test service integration
describe('UserService Integration', () => {
let userService: UserService;
beforeAll(async () => {
// Setup real dependencies for integration testing
const config = new ConfigurationManager();
config.addJsonFile('test-settings.json');
const httpClient = new HttpClient(config.getValue('api:baseUrl'));
const repository = new UserRepository(httpClient);
userService = new UserService(repository);
});
it('should handle real API interaction', async () => {
// Test with real API calls
const user = await userService.getUser(1);
expect(user).toBeDefined();
});
});Security Best Practices
Input Validation
typescript
// ✅ Always validate inputs
function validateUserInput(input: unknown): UserInput {
if (!TypeGuard.isObject(input)) {
throw new ArgumentException('input', 'Input must be an object');
}
if (!TypeGuard.hasProperty(input, 'email') || !TypeGuard.isString(input.email)) {
throw new ArgumentException('input.email', 'Email must be a string');
}
if (StringExtensions.isNullOrWhiteSpace(input.email)) {
throw new ArgumentException('input.email', 'Email cannot be empty');
}
return input as UserInput;
}Sensitive Data Handling
typescript
// ✅ Don't log sensitive information
class AuthService {
private logger = LoggerManager.getLogger('AuthService');
async login(credentials: LoginCredentials): Promise<AuthResult> {
// ❌ Don't log passwords
// this.logger.info('Login attempt', credentials);
// ✅ Log only non-sensitive data
this.logger.info('Login attempt', { email: credentials.email });
try {
const result = await this.authenticate(credentials);
// ✅ Don't log tokens in production
if (process.env.NODE_ENV !== 'production') {
this.logger.debug('Login successful', { token: result.token });
} else {
this.logger.info('Login successful', { userId: result.user.id });
}
return result;
} catch (error) {
this.logger.error('Login failed', error, { email: credentials.email });
throw error;
}
}
}Common Anti-Patterns to Avoid
❌ Don't bypass service location
typescript
// Wrong
class UserService {
private httpClient = new HttpClient(); // Direct instantiation
}
// Right
class UserService {
private httpClient = ServiceLocator.get<HttpClient>('httpClient');
}❌ Don't ignore error handling
typescript
// Wrong
async function getUser(id: number) {
return await api.get(`/users/${id}`); // No error handling
}
// Right
async function getUser(id: number): Promise<User> {
try {
return await api.get<User>(`/users/${id}`);
} catch (error) {
logger.error('Failed to fetch user', error, { userId: id });
throw new InvalidOperationException(`Unable to fetch user with ID: ${id}`);
}
}❌ Don't mix concerns
typescript
// Wrong - mixing data access with business logic
class UserController {
async createUser(userData: any) {
// Validation, database access, email sending all in one place
if (!userData.email) throw new Error('Email required');
const user = await database.users.create(userData);
await emailService.sendWelcome(user.email);
return user;
}
}
// Right - separated concerns
class UserController {
constructor(private userService: UserService) {}
async createUser(userData: CreateUserRequest) {
return await this.userService.createUser(userData);
}
}Next Steps
- Performance Guide - Optimize your applications
- Testing Guide - Comprehensive testing strategies
- Security Guide - Security best practices