Skip to content

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 flexible

Exception 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