← Back to Blog

Theme Factory: Architecting a Scalable Backend for Dynamic Theme Management

Introduction

In the modern web development landscape, the ability to provide dynamic, customizable themes has become a critical differentiator for SaaS applications and white-label solutions. This case study explores the architecture and implementation of Theme Factory, a robust NestJS-based API designed to manage MUI theme configurations with enterprise-grade security, scalability, and maintainability.

The project addresses a common challenge in multi-tenant applications: how to efficiently store, version, and serve dynamic theme configurations while maintaining data integrity, security, and performance. This backend solution serves as the foundation for a comprehensive theme management system, supporting both a “Theme Designer” frontend application and multiple client applications.

Problem Statement & Requirements

The Challenge

Modern applications require sophisticated theming capabilities that go beyond simple CSS variables. When building white-label solutions or multi-tenant platforms, organizations need:

Technical Requirements

The solution needed to support:

System Architecture & Design

Framework Choice: Why NestJS?

After evaluating Express.js, Fastify, and other Node.js frameworks, NestJS was selected for several compelling reasons:

1. Enterprise-Grade Architecture

// Clean module-based architecture
@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: parseInt(configService.get('DB_PORT') || '5432'),
        username: configService.get('DB_USERNAME'),
        password: configService.get('POSTGRES_PASSWORD'),
        database: configService.get('POSTGRES_DB'),
        autoLoadEntities: true,
        synchronize: configService.get('NODE_ENV') !== 'production',
        logging: configService.get('NODE_ENV') === 'development',
      }),
    }),
    ThemesModule,
    UsersModule,
    AuthModule,
    CommonModule,
  ],
})
export class AppModule {}

2. Dependency Injection & Testability NestJS’s built-in DI container enables easy mocking and testing, crucial for maintaining code quality in enterprise applications.

3. TypeScript-First Approach Full TypeScript support with decorators and metadata reflection provides compile-time safety and excellent developer experience.

4. Built-in Security Features Integration with Passport.js, JWT, and validation pipes out of the box.

Database Design: PostgreSQL Schema

The database schema was designed with scalability and performance in mind:

-- Users table for authentication and audit trails
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL,
  first_name VARCHAR(100) NOT NULL,
  last_name VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Themes table with JSONB for flexible configuration storage
CREATE TABLE themes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(100) UNIQUE NOT NULL,
  description TEXT,
  theme_config JSONB NOT NULL,
  preview_image VARCHAR(500),
  tags TEXT[],
  is_active BOOLEAN DEFAULT true,
  created_by_id UUID REFERENCES users(id),
  updated_by_id UUID REFERENCES users(id),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for performance optimization
CREATE INDEX idx_themes_created_by ON themes(created_by_id);
CREATE INDEX idx_themes_tags ON themes USING GIN(tags);
CREATE INDEX idx_themes_config ON themes USING GIN(theme_config);
CREATE INDEX idx_themes_active ON themes(is_active);

Key Design Decisions:

  1. JSONB for Theme Configuration: PostgreSQL’s JSONB type provides excellent performance for querying nested theme configurations while maintaining flexibility.

  2. Soft Deletes: Using is_active boolean instead of hard deletes preserves data integrity and enables audit trails.

  3. Audit Trail: Tracking created_by_id and updated_by_id ensures accountability and compliance.

  4. GIN Indexes: Optimized for array and JSONB queries, essential for tag-based filtering and theme configuration searches.

Authentication Strategy: JWT with Passport

The authentication system implements a secure JWT-based approach:

// JWT Strategy implementation
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: JwtPayload): Promise<User> {
    const { sub: userId } = payload;
    const user = await this.usersService.findOne(userId);
    
    if (!user || !user.isActive) {
      throw new UnauthorizedException('User not found or inactive');
    }
    
    return user;
  }
}

Security Features:

Technical Implementation Details

Entity Definitions: TypeORM Models

The core entities demonstrate clean separation of concerns and proper relationship modeling:

@Entity('themes')
export class Theme {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  name: string;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ type: 'jsonb' })
  themeConfig: any;

  @Column({ nullable: true })
  previewImage: string;

  @Column({ type: 'simple-array', nullable: true })
  tags: string[];

  @Column({ default: true })
  isActive: boolean;

  @Column({ type: 'uuid' })
  createdById: string;

  @Column({ type: 'uuid' })
  updatedById: string;

  @ManyToOne(() => User)
  @JoinColumn({ name: 'createdById' })
  createdBy: User;

  @ManyToOne(() => User)
  @JoinColumn({ name: 'updatedById' })
  updatedBy: User;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Key Features:

Service Layer Architecture

The service layer implements clean business logic with proper error handling:

@Injectable()
export class ThemesService {
  private readonly logger = new Logger(ThemesService.name);

  constructor(
    @InjectRepository(Theme)
    private readonly themeRepository: Repository<Theme>,
  ) {}

  async findAll(query: ThemeQueryDto): Promise<{
    data: Theme[];
    total: number;
    page: number;
    limit: number;
  }> {
    const {
      page = 1,
      limit = 10,
      search,
      createdBy,
      tags,
      sortBy = 'createdAt',
      sortOrder = 'DESC',
    } = query;
    
    const skip = (page - 1) * limit;

    const queryBuilder = this.themeRepository
      .createQueryBuilder('theme')
      .leftJoinAndSelect('theme.createdBy', 'createdBy')
      .leftJoinAndSelect('theme.updatedBy', 'updatedBy')
      .where('theme.isActive = :isActive', { isActive: true });

    // Dynamic filtering
    if (search) {
      queryBuilder.andWhere(
        '(theme.name ILIKE :search OR theme.description ILIKE :search)',
        { search: `%${search}%` },
      );
    }

    if (createdBy) {
      queryBuilder.andWhere('theme.createdById = :createdBy', { createdBy });
    }

    if (tags && tags.length > 0) {
      queryBuilder.andWhere('theme.tags && :tags', { tags });
    }

    queryBuilder.orderBy(`theme.${sortBy}`, sortOrder);
    queryBuilder.skip(skip).take(limit);

    const [data, total] = await queryBuilder.getManyAndCount();

    return { data, total, page, limit };
  }

  private handleDbErrors(error: any) {
    this.logger.error('Database error:', error);

    if (error instanceof QueryFailedError) {
      const dbError = error.driverError as DatabaseError;
      switch (dbError.code) {
        case '23505':
          if (dbError.detail?.includes('name')) {
            throw new ConflictException('Theme name already exists');
          }
          throw new ConflictException('Record already exists');
        case '23503':
          throw new ConflictException('Cannot delete, has related records');
        case '23514':
          throw new ConflictException('Data does not meet constraints');
        default:
          break;
      }
    }

    throw new InternalServerErrorException('Internal server error');
  }
}

Architecture Benefits:

DTO & Validation Strategy

The validation system ensures data integrity and API consistency:

export class CreateThemeDto {
  @ApiProperty({
    description: 'Unique name of the theme',
    example: 'Dark Purple Theme',
  })
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name: string;

  @ApiProperty({
    description: 'Description of the theme',
    example: 'A dark theme with purple accents for modern applications',
    required: false,
  })
  @IsOptional()
  @IsString()
  @MaxLength(500)
  description?: string;

  @ApiProperty({
    description: 'Configuration of the theme compatible with MUI ThemeOptions',
  })
  @IsObject()
  @ValidateNested()
  @Type(() => MuiThemeConfigDto)
  themeConfig: MuiThemeConfigDto;

  @ApiProperty({
    description: 'Tags to categorize the theme',
    example: ['dark', 'purple', 'modern', 'enterprise'],
    required: false,
  })
  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  @ArrayMaxSize(10)
  @MaxLength(30, { each: true })
  tags?: string[];
}

Validation Features:

API Design & Documentation

RESTful Endpoint Structure

The API follows RESTful conventions with clear resource modeling:

Authentication:
POST   /api/auth/register    - User registration
POST   /api/auth/login       - User authentication

Users (Protected):
GET    /api/users            - List users
GET    /api/users/:id        - Get user by ID
PATCH  /api/users/:id        - Update user

Themes (Protected):
POST   /api/themes/create    - Create theme
GET    /api/themes           - List themes with filtering
GET    /api/themes/:id       - Get theme by ID
PATCH  /api/themes/:id       - Update theme
DELETE /api/themes/:id       - Soft delete theme

Request/Response Schemas

The API provides consistent, well-documented schemas:

// Theme creation request
{
  "name": "Dark Purple Theme",
  "description": "A dark theme with purple accents",
  "themeConfig": {
    "palette": {
      "mode": "dark",
      "primary": {
        "main": "#9c27b0",
        "light": "#ba68c8",
        "dark": "#6a1b7a"
      },
      "secondary": {
        "main": "#ff5722"
      }
    },
    "typography": {
      "fontFamily": "Inter, sans-serif"
    },
    "shape": {
      "borderRadius": 12
    }
  },
  "tags": ["dark", "purple", "modern"]
}

// Paginated response
{
  "data": [...],
  "total": 150,
  "page": 1,
  "limit": 10
}

Error Handling Strategy

Comprehensive error handling ensures consistent API responses:

// Global validation pipe configuration
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: {
      enableImplicitConversion: true,
    },
  }),
);

// Standardized error responses
{
  "statusCode": 400,
  "message": ["name should not be empty", "themeConfig must be an object"],
  "error": "Bad Request"
}

Security & Performance

JWT Token Management

Secure token handling with proper configuration:

// JWT Module configuration
JwtModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    secret: configService.get('JWT_SECRET'),
    signOptions: {
      expiresIn: configService.get('JWT_EXPIRES_IN', '24h'),
    },
  }),
})

Security Measures:

Password Security

Bcrypt implementation for secure password hashing:

// Password hashing in user service
async create(createUserDto: CreateUserDto): Promise<User> {
  const hashedPassword = await bcrypt.hash(
    createUserDto.password,
    this.saltRounds,
  );
  
  const user = this.userRepository.create({
    ...createUserDto,
    password: hashedPassword,
  });
  
  return this.userRepository.save(user);
}

// Password validation
async validatePassword(email: string, password: string): Promise<User | null> {
  const user = await this.userRepository.findOne({
    where: { email },
    select: ['id', 'email', 'password', 'firstName', 'lastName', 'isActive'],
  });

  if (user && (await bcrypt.compare(password, user.password))) {
    return user;
  }

  return null;
}

Query Optimization

Performance optimization through strategic indexing and query design:

-- Performance indexes
CREATE INDEX CONCURRENTLY idx_themes_search 
ON themes USING gin(to_tsvector('english', name || ' ' || COALESCE(description, '')));

CREATE INDEX CONCURRENTLY idx_themes_created_at 
ON themes(created_at DESC) WHERE is_active = true;

-- Optimized query for theme listing
SELECT 
  t.*,
  u1.first_name as creator_first_name,
  u1.last_name as creator_last_name,
  u2.first_name as updater_first_name,
  u2.last_name as updater_last_name
FROM themes t
LEFT JOIN users u1 ON t.created_by_id = u1.id
LEFT JOIN users u2 ON t.updated_by_id = u2.id
WHERE t.is_active = true
  AND (t.name ILIKE $1 OR t.description ILIKE $1)
  AND t.tags && $2
ORDER BY t.created_at DESC
LIMIT $3 OFFSET $4;

Testing & Deployment

Testing Strategy

Comprehensive testing approach covering unit, integration, and e2e tests:

// Unit test example
describe('ThemesService', () => {
  let service: ThemesService;
  let repository: Repository<Theme>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ThemesService,
        {
          provide: getRepositoryToken(Theme),
          useClass: Repository,
        },
      ],
    }).compile();

    service = module.get<ThemesService>(ThemesService);
    repository = module.get<Repository<Theme>>(getRepositoryToken(Theme));
  });

  it('should create a theme', async () => {
    const createThemeDto = {
      name: 'Test Theme',
      themeConfig: { palette: { mode: 'dark' } },
    };
    const userId = 'test-user-id';
    
    jest.spyOn(repository, 'create').mockReturnValue(createThemeDto as Theme);
    jest.spyOn(repository, 'save').mockResolvedValue(createThemeDto as Theme);
    
    const result = await service.create(createThemeDto, userId);
    
    expect(result).toEqual(createThemeDto);
    expect(repository.create).toHaveBeenCalledWith({
      ...createThemeDto,
      createdById: userId,
      updatedById: userId,
    });
  });
});

Docker Deployment

Containerized deployment with Docker Compose:

version: '3.8'

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
      - DB_PORT=5432
      - DB_USERNAME=${DB_USERNAME}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:14.3
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:

Scalability Considerations

Multi-tenant Architecture Preparation

The system is designed to support multi-tenant scenarios:

// Future multi-tenant implementation
@Entity('themes')
export class Theme {
  // ... existing fields
  
  @Column({ type: 'uuid', nullable: true })
  tenantId: string;
  
  @ManyToOne(() => Tenant)
  @JoinColumn({ name: 'tenantId' })
  tenant: Tenant;
}

// Tenant-aware service methods
async findAllByTenant(tenantId: string, query: ThemeQueryDto) {
  const queryBuilder = this.themeRepository
    .createQueryBuilder('theme')
    .where('theme.tenantId = :tenantId', { tenantId })
    .andWhere('theme.isActive = :isActive', { isActive: true });
    
  // ... rest of implementation
}

Performance Monitoring

Built-in monitoring and logging capabilities:

// Performance monitoring middleware
@Injectable()
export class PerformanceMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    const start = Date.now();
    
    res.on('finish', () => {
      const duration = Date.now() - start;
      this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
    });
    
    next();
  }
}

Horizontal Scaling Possibilities

The architecture supports horizontal scaling:

  1. Stateless API: No session storage, JWT-based authentication
  2. Database Connection Pooling: Optimized connection management
  3. Load Balancer Ready: Standard HTTP endpoints
  4. Container Orchestration: Docker-based deployment

Results & Metrics

Performance Benchmarks

The implemented solution achieves excellent performance metrics:

Integration Success

The API successfully integrates with:

Scalability Achievements

Lessons Learned & Future Enhancements

Architecture Decisions Retrospective

What Worked Well:

  1. NestJS Framework: Excellent choice for enterprise applications
  2. PostgreSQL JSONB: Perfect for flexible theme configuration storage
  3. TypeORM Integration: Seamless database operations with TypeScript
  4. Validation Strategy: Comprehensive input validation prevents data corruption

Areas for Improvement:

  1. Caching Layer: Redis integration for frequently accessed themes
  2. Rate Limiting: API rate limiting for production environments
  3. Monitoring: Enhanced metrics and alerting systems
  4. Migration Strategy: Automated database migration pipeline

Next Iteration Plans

Phase 2 Enhancements:

  1. Theme Versioning: Git-like versioning system for theme configurations
  2. Theme Templates: Pre-built theme templates for rapid deployment
  3. Advanced Search: Full-text search with Elasticsearch integration
  4. Theme Analytics: Usage tracking and performance metrics
  5. Multi-language Support: Internationalization for theme metadata

Technical Improvements:

// Planned caching implementation
@Injectable()
export class ThemeCacheService {
  constructor(
    private readonly redisService: RedisService,
    private readonly themesService: ThemesService,
  ) {}

  async getTheme(id: string): Promise<Theme | null> {
    const cached = await this.redisService.get(`theme:${id}`);
    if (cached) {
      return JSON.parse(cached);
    }

    const theme = await this.themesService.findOne(id);
    if (theme) {
      await this.redisService.setex(`theme:${id}`, 3600, JSON.stringify(theme));
    }

    return theme;
  }
}

Conclusion

The Theme Factory project demonstrates the power of modern backend architecture in solving real-world problems. By leveraging NestJS, PostgreSQL, and TypeORM, we’ve created a robust, scalable, and maintainable solution for dynamic theme management.

Key achievements include:

This case study showcases the importance of thoughtful architecture decisions, proper tool selection, and attention to detail in building production-ready backend systems. The lessons learned and patterns established serve as a foundation for future projects requiring similar levels of complexity and reliability.

The Theme Factory API stands as a testament to modern backend development best practices, providing a solid foundation for dynamic theme management in multi-tenant applications while maintaining the flexibility and performance required for real-world usage.


This project demonstrates the power of combining modern backend technologies to solve complex design system challenges. The code is available on GitHub for further exploration and contribution.