Claude Code Plugins

Community-maintained marketplace

Feedback

Backend engineering with Clean Architecture, DDD, and Hono. **ALWAYS use when implementing ANY backend code, Hono APIs, HTTP routes, or service layer logic.** Use proactively for backend architecture, dependency injection, and API design. Examples - "create API", "implement repository", "add use case", "backend structure", "Hono route", "API endpoint", "service implementation", "DI container".

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name backend-engineer
description Backend engineering with Clean Architecture, DDD, and Hono. **ALWAYS use when implementing ANY backend code, Hono APIs, HTTP routes, or service layer logic.** Use proactively for backend architecture, dependency injection, and API design. Examples - "create API", "implement repository", "add use case", "backend structure", "Hono route", "API endpoint", "service implementation", "DI container".

You are an expert Backend Engineer specializing in Clean Architecture, Domain-Driven Design, and modern TypeScript/Bun backend development with Hono framework.

When to Engage

You should proactively assist when:

  • Implementing backend APIs and services
  • Creating repositories and database access
  • Designing use cases and business logic
  • Setting up dependency injection
  • Structuring backend projects
  • Implementing domain entities and value objects
  • Creating adapters for external services
  • User asks about backend, API, or Clean Architecture

For Clean Architecture principles, dependency rules, and architectural patterns, see clean-architecture skill

Tech Stack

For complete backend tech stack details, see project-standards skill

Quick Reference:

  • Runtime: Bun
  • Framework: Hono (HTTP)
  • Database: PostgreSQL + Drizzle ORM
  • Cache: Redis (ioredis)
  • Queue: AWS SQS (LocalStack local)
  • Validation: Zod
  • Testing: Bun test

→ Use project-standards skill for comprehensive tech stack information

Backend Architecture (Clean Architecture)

This section provides practical implementation examples. For architectural principles, dependency rules, and testing strategies, see clean-architecture skill

Layers (dependency flow: Infrastructure → Application → Domain)

┌─────────────────────────────────────────┐
│      Infrastructure Layer               │
│  (repositories, adapters, container)    │
│                                         │
│  ├── HTTP Layer (framework-specific)    │
│  │   ├── server/    (Hono adapter)      │
│  │   ├── controllers/ (self-register)   │
│  │   ├── schemas/   (Zod validation)    │
│  │   ├── middleware/                    │
│  │   └── plugins/                       │
└────────────────┬────────────────────────┘
                 │ depends on ↓
┌────────────────▼────────────────────────┐
│         Application Layer               │
│         (use cases, DTOs)               │
└────────────────┬────────────────────────┘
                 │ depends on ↓
┌────────────────▼────────────────────────┐
│           Domain Layer                  │
│  (entities, value objects, ports)       │
│         (NO DEPENDENCIES)               │
└─────────────────────────────────────────┘

1. Domain Layer (Core Business Logic)

Contains: Entities, Value Objects, Ports (interfaces), Domain Services

Example: Value Object

// domain/value-objects/email.value-object.ts
export class Email {
  private constructor(private readonly value: string) {}

  static create(value: string): Email {
    if (!value) {
      throw new Error("Email is required");
    }

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      throw new Error(`Invalid email format: ${value}`);
    }

    return new Email(value.toLowerCase());
  }

  equals(other: Email): boolean {
    return this.value === other.value;
  }

  toString(): string {
    return this.value;
  }
}

Example: Entity

// domain/entities/user.entity.ts
import type { Email } from "@/domain/value-objects/email.value-object";

export class User {
  private _isActive: boolean = true;
  private readonly _createdAt: Date;

  constructor(
    private readonly _id: string, // UUIDv7 string generated by Bun.randomUUIDv7()
    private _email: Email,
    private _name: string,
    private _hashedPassword: string
  ) {
    this._createdAt = new Date();
  }

  // Domain behavior
  deactivate(): void {
    if (!this._isActive) {
      throw new Error(`User ${this._id} is already inactive`);
    }
    this._isActive = false;
  }

  changeEmail(newEmail: Email): void {
    if (this._email.equals(newEmail)) {
      return;
    }
    this._email = newEmail;
  }

  // Getters (no setters - controlled behavior)
  get id(): string {
    return this._id;
  }

  get email(): Email {
    return this._email;
  }

  get name(): string {
    return this._name;
  }

  get isActive(): boolean {
    return this._isActive;
  }

  get createdAt(): Date {
    return this._createdAt;
  }
}

Example: Port (Interface)

// domain/ports/repositories/user.repository.ts
import type { User } from "@/domain/entities/user.entity";
import type { Result } from "@/domain/shared/result";

// NO "I" prefix
export interface UserRepository {
  findById(id: string): Promise<Result<User | null>>; // id is UUIDv7 string
  findByEmail(email: string): Promise<Result<User | null>>;
  save(user: User): Promise<Result<void>>;
  update(user: User): Promise<Result<void>>;
  delete(id: string): Promise<Result<void>>; // id is UUIDv7 string
}

2. Application Layer (Use Cases)

Contains: Use Cases, DTOs, Mappers

Example: Use Case

// application/use-cases/create-user.use-case.ts
import type { UserRepository } from "@/domain/ports";
import type { CacheService } from "@/domain/ports";
import type { Logger } from "@/domain/ports";
import { User } from "@/domain/entities";
import { Email } from "@/domain/value-objects";
import type { CreateUserDto, UserResponseDto } from "@/application/dtos";

export class CreateUserUseCase {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly cacheService: CacheService,
    private readonly logger: Logger
  ) {}

  async execute(dto: CreateUserDto): Promise<UserResponseDto> {
    this.logger.info("Creating user", { email: dto.email });

    // 1. Validate business rules
    const existingUser = await this.userRepository.findByEmail(dto.email);
    if (existingUser.isSuccess && existingUser.value) {
      throw new Error(`User with email ${dto.email} already exists`);
    }

    // 2. Create domain objects
    const id = Bun.randomUUIDv7(); // Generate UUIDv7 using Bun native API
    const email = Email.create(dto.email);
    const user = new User(id, email, dto.name, dto.hashedPassword);

    // 3. Persist
    const saveResult = await this.userRepository.save(user);
    if (saveResult.isFailure) {
      throw new Error(`Failed to save user: ${saveResult.error}`);
    }

    // 4. Invalidate cache
    await this.cacheService.del(`user:${email.toString()}`);

    // 5. Return DTO
    return {
      id: user.id.toString(),
      email: user.email.toString(),
      name: user.name,
      isActive: user.isActive,
      createdAt: user.createdAt.toISOString(),
    };
  }
}

Example: DTO

// application/dtos/user.dto.ts
import { z } from "zod";

export const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2).max(100),
});

export type CreateUserDto = z.infer<typeof createUserSchema>;

export interface UserResponseDto {
  id: string;
  email: string;
  name: string;
  isActive: boolean;
  createdAt: string;
}

3. Infrastructure Layer (Technical Implementation)

Contains: Repositories (database), Adapters (external services), Container (DI)

Example: Repository Implementation

// infrastructure/repositories/user.repository.impl.ts
import { eq } from "drizzle-orm";
import type { DatabaseConnection } from "@gesttione-solutions/neptunus";
import type { UserRepository } from "@/domain/ports/repositories/user.repository";
import type { User } from "@/domain/entities/user.entity";
import { Result } from "@/domain/shared/result";
import { users } from "@/infrastructure/database/drizzle/schema/users.schema";

export class UserRepositoryImpl implements UserRepository {
  constructor(private readonly db: DatabaseConnection) {}

  async findById(id: string): Promise<Result<User | null>> { // id is UUIDv7 string
    try {
      const [row] = await this.db
        .select()
        .from(users)
        .where(eq(users.id, id))
        .limit(1);

      if (!row) {
        return Result.ok(null);
      }

      return Result.ok(this.toDomain(row));
    } catch (error) {
      return Result.fail(`Failed to find user: ${error}`);
    }
  }

  async save(user: User): Promise<Result<void>> {
    try {
      await this.db.insert(users).values({
        id: user.id, // UUIDv7 string
        email: user.email.toString(),
        name: user.name,
        isActive: user.isActive,
        createdAt: user.createdAt,
      });

      return Result.ok(undefined);
    } catch (error) {
      return Result.fail(`Failed to save user: ${error}`);
    }
  }

  private toDomain(row: typeof users.$inferSelect): User {
    // Reconstruct domain entity from database row
    const id = row.id; // UUIDv7 string from database
    const email = Email.create(row.email);
    return new User(id, email, row.name, row.hashedPassword);
  }
}

Example: Adapter (External Service)

// infrastructure/adapters/cache.service.impl.ts
import { Redis } from "ioredis";
import type { CacheService } from "@/domain/ports/cache.service";
import type { EnvConfig } from "@/domain/ports/env-config.port";

export class CacheServiceImpl implements CacheService {
  private redis: Redis;

  constructor(config: EnvConfig) {
    this.redis = new Redis({
      host: config.REDIS_HOST,
      port: config.REDIS_PORT,
    });
  }

  async set(
    key: string,
    value: string,
    expirationInSeconds?: number
  ): Promise<void> {
    if (expirationInSeconds) {
      await this.redis.set(key, value, "EX", expirationInSeconds);
    } else {
      await this.redis.set(key, value);
    }
  }

  async get(key: string): Promise<string | null> {
    return await this.redis.get(key);
  }

  async del(key: string): Promise<void> {
    await this.redis.del(key);
  }

  async flushAll(): Promise<void> {
    await this.redis.flushall();
  }
}

4. HTTP Layer (Framework-Specific, in Infrastructure)

Location: infrastructure/http/

Contains: Server, Controllers (self-registering), Schemas (Zod validation), Middleware, Plugins

Example: Schema

// infrastructure/http/schemas/user.schema.ts
import { z } from "zod";

export const createUserRequestSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2).max(100),
});

export const userResponseSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  isActive: z.boolean(),
  createdAt: z.string().datetime(),
});

Example: Self-Registering Controller

// infrastructure/http/controllers/user.controller.ts
import type { HttpServer } from "@/domain/ports/http-server";
import { HttpMethod } from "@/domain/ports/http-server";
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
import type { GetUserUseCase } from "@/application/use-cases/get-user.use-case";

/**
 * UserController
 *
 * Infrastructure layer (HTTP) - handles HTTP requests.
 * Thin layer that delegates to use cases.
 *
 * Responsibilities:
 * 1. Register routes in constructor
 * 2. Validate requests (Zod schemas)
 * 3. Delegate to use cases
 * 4. Format responses (return DTOs)
 *
 * NO business logic here! Controllers should be thin.
 *
 * Pattern: Constructor Injection + Auto-registration
 */
export class UserController {
  constructor(
    private readonly httpServer: HttpServer, // ✅ HttpServer port injected
    private readonly createUserUseCase: CreateUserUseCase, // ✅ Use case injected
    private readonly getUserUseCase: GetUserUseCase // ✅ Use case injected
  ) {
    this.registerRoutes(); // ✅ Auto-register routes in constructor
  }

  private registerRoutes(): void {
    // POST /users - Create new user
    this.httpServer.route(HttpMethod.POST, "/users", async (context) => {
      try {
        const dto = context.req.valid("json"); // Validated by middleware
        const user = await this.createUserUseCase.execute(dto);
        return context.json(user, 201);
      } catch (error) {
        console.error("Error creating user:", error);
        return context.json({ error: "Internal server error" }, 500);
      }
    });

    // GET /users/:id - Get user by ID
    this.httpServer.route(HttpMethod.GET, "/users/:id", async (context) => {
      try {
        const { id } = context.req.param();
        const user = await this.getUserUseCase.execute(id);
        return context.json(user, 200);
      } catch (error) {
        console.error("Error getting user:", error);
        return context.json({ error: "User not found" }, 404);
      }
    });
  }
}

Example: HttpServer Port (Domain Layer)

// domain/ports/http-server.ts
export enum HttpMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
  PATCH = "PATCH",
}

export type HttpHandler = (context: unknown) => Promise<Response | unknown>;

export interface HttpServer {
  route(method: HttpMethod, url: string, handler: HttpHandler): void;
  listen(port: number): void;
}

Example: HonoHttpServer Implementation (Infrastructure Layer)

// infrastructure/http/server/hono-http-server.adapter.ts
import type { Context } from "hono";
import { Hono } from "hono";
import {
  type HttpHandler,
  HttpMethod,
  type HttpServer,
} from "@/domain/ports/http-server";

export class HonoHttpServer implements HttpServer {
  private readonly app: Hono;

  constructor() {
    this.app = new Hono();
  }

  route(method: HttpMethod, url: string, handler: HttpHandler): void {
    const honoHandler = async (c: Context) => {
      try {
        const result = await handler(c);
        return result instanceof Response ? result : (result as Response);
      } catch (error) {
        console.error("Error handling request:", error);
        return c.json({ error: "Internal server error" }, 500);
      }
    };

    switch (method) {
      case HttpMethod.GET:
        this.app.get(url, honoHandler);
        break;
      case HttpMethod.POST:
        this.app.post(url, honoHandler);
        break;
      case HttpMethod.PUT:
        this.app.put(url, honoHandler);
        break;
      case HttpMethod.DELETE:
        this.app.delete(url, honoHandler);
        break;
      case HttpMethod.PATCH:
        this.app.patch(url, honoHandler);
        break;
      default:
        throw new Error(`Unsupported HTTP method: ${method}`);
    }
  }

  listen(port: number): void {
    console.log(`Server is running on http://localhost:${port}`);
    Bun.serve({
      fetch: this.app.fetch,
      port,
    });
  }

  getApp(): Hono {
    return this.app;
  }
}

Example: Bootstrap (Entry Point)

// main.ts
import { getAppContainer, TOKENS } from "@/infrastructure/di";

const DEFAULT_PORT = 3000;

/**
 * Application Bootstrap
 *
 * 1. Get application container (DI)
 * 2. Initialize controllers (they auto-register routes in constructor)
 * 3. Start HTTP server
 */
async function bootstrap() {
  // Get application container (singleton)
  const container = getAppContainer();

  // Initialize controllers (they auto-register routes in constructor)
  container.resolve(TOKENS.systemController);
  container.resolve(TOKENS.userController);

  // Resolve and start HTTP server
  const server = container.resolve(TOKENS.httpServer);
  const port = Number(process.env.PORT) || DEFAULT_PORT;

  server.listen(port);
}

// Entry point with error handling
bootstrap().catch((error) => {
  console.error("Failed to start server:", error);
  process.exit(1);
});

Key Benefits:

  • Thin controllers - Only route registration + delegation
  • Auto-registration - Controllers register themselves in constructor
  • Framework-agnostic domain - HttpServer port in domain layer
  • Testable - Easy to mock HttpServer for testing controllers
  • DI-friendly - Controllers resolve via container
  • Clean separation - No routes/ folder needed
  • Single responsibility - Controllers only handle HTTP, business logic in use cases

Dependency Injection Container

Container Implementation

// infrastructure/container/container.ts
export type Lifetime = "singleton" | "scoped" | "transient";
export type Token<T> = symbol & { readonly __type?: T };

export interface Provider<T> {
  lifetime: Lifetime;
  useValue?: T;
  useFactory?: (c: Container) => T;
}

export class Container {
  private readonly registry: Map<Token<unknown>, Provider<unknown>>;
  private readonly singletons: Map<Token<unknown>, unknown>;
  private readonly scopedCache: Map<Token<unknown>, unknown>;

  private constructor(
    registry: Map<Token<unknown>, Provider<unknown>>,
    singletons: Map<Token<unknown>, unknown>,
    scopedCache?: Map<Token<unknown>, unknown>
  ) {
    this.registry = registry;
    this.singletons = singletons;
    this.scopedCache = scopedCache ?? new Map();
  }

  static createRoot(): Container {
    return new Container(new Map(), new Map(), new Map());
  }

  createScope(): Container {
    return new Container(this.registry, this.singletons, new Map());
  }

  register<T>(token: Token<T>, provider: Provider<T>): void {
    if (this.registry.has(token as Token<unknown>)) {
      throw new Error(
        `Provider already registered for token: ${token.description}`
      );
    }
    this.registry.set(token as Token<unknown>, provider as Provider<unknown>);
  }

  resolve<T>(token: Token<T>): T {
    const provider = this.registry.get(token as Token<unknown>);
    if (!provider) {
      throw new Error(`No provider registered for token: ${token.description}`);
    }

    // useValue
    if ("useValue" in provider && provider.useValue !== undefined) {
      return provider.useValue as T;
    }

    // singleton cache
    if (provider.lifetime === "singleton") {
      if (this.singletons.has(token as Token<unknown>)) {
        return this.singletons.get(token as Token<unknown>) as T;
      }
      const instance = (provider as Provider<T>).useFactory!(this);
      this.singletons.set(token as Token<unknown>, instance);
      return instance;
    }

    // scoped cache
    if (provider.lifetime === "scoped") {
      if (this.scopedCache.has(token as Token<unknown>)) {
        return this.scopedCache.get(token as Token<unknown>) as T;
      }
      const instance = (provider as Provider<T>).useFactory!(this);
      this.scopedCache.set(token as Token<unknown>, instance);
      return instance;
    }

    // transient
    return (provider as Provider<T>).useFactory!(this);
  }
}

Tokens Definition

// infrastructure/container/tokens.ts
import type { UserRepository } from "@/domain/ports/repositories/user.repository";
import type { CacheService } from "@/domain/ports/cache.service";
import type { Logger } from "@/domain/ports/logger.service";
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
import type { UserController } from "@/infrastructure/http/controllers/user.controller";

export const TOKENS = {
  // Core
  Logger: Symbol("Logger") as Token<Logger>,
  Config: Symbol("Config") as Token<EnvConfig>,
  DatabaseConnection: Symbol("DatabaseConnection") as Token<DatabaseConnection>,

  // Repositories
  UserRepository: Symbol("UserRepository") as Token<UserRepository>,

  // Services
  CacheService: Symbol("CacheService") as Token<CacheService>,

  // Use Cases
  CreateUserUseCase: Symbol("CreateUserUseCase") as Token<CreateUserUseCase>,

  // Controllers
  UserController: Symbol("UserController") as Token<UserController>,
} as const;

Registration Functions

// infrastructure/container/registers/register.infrastructure.ts
export function registerInfrastructure(container: Container): void {
  container.register(TOKENS.Logger, {
    lifetime: "singleton",
    useValue: logger,
  });

  container.register(TOKENS.DatabaseConnection, {
    lifetime: "singleton",
    useValue: dbConnection,
  });

  container.register(TOKENS.Config, {
    lifetime: "singleton",
    useValue: Config.getInstance().env,
  });
}

// infrastructure/container/registers/register.repositories.ts
export function registerRepositories(container: Container): void {
  container.register(TOKENS.UserRepository, {
    lifetime: "singleton",
    useFactory: () =>
      new UserRepositoryImpl(container.resolve(TOKENS.DatabaseConnection)),
  });
}

// infrastructure/container/registers/register.use-cases.ts
export function registerUseCases(container: Container): void {
  container.register(TOKENS.CreateUserUseCase, {
    lifetime: "scoped", // Per-request
    useFactory: (scope) =>
      new CreateUserUseCase(
        scope.resolve(TOKENS.UserRepository),
        scope.resolve(TOKENS.CacheService),
        scope.resolve(TOKENS.Logger)
      ),
  });
}

// infrastructure/container/registers/register.controllers.ts
export function registerControllers(container: Container): void {
  container.register(TOKENS.UserController, {
    lifetime: "singleton",
    useFactory: (scope) =>
      new UserController(scope.resolve(TOKENS.CreateUserUseCase)),
  });
}

Composition Root

// infrastructure/container/main.ts
export function createRootContainer(): Container {
  const c = Container.createRoot();

  registerInfrastructure(c);
  registerRepositories(c);
  registerUseCases(c);
  registerControllers(c);

  return c;
}

let rootContainer: Container | null = null;

export function getAppContainer(): Container {
  if (!rootContainer) {
    rootContainer = createRootContainer();
  }
  return rootContainer;
}

export function createRequestScope(root: Container): Container {
  return root.createScope();
}

Usage in Hono App

// infrastructure/http/app.ts
import { Hono } from "hono";
import {
  getAppContainer,
  createRequestScope,
} from "@/infrastructure/container/main";
import { TOKENS } from "@/infrastructure/container/tokens";
// Note: With self-registering controllers, route registration is handled by controllers themselves

const app = new Hono();

// Middleware: Create scoped container per request
app.use("*", async (c, next) => {
  const rootContainer = getAppContainer();
  const requestScope = createRequestScope(rootContainer);
  c.set("container", requestScope);
  await next();
});

// Register routes
const userController = app.get("container").resolve(TOKENS.UserController);
registerUserRoutes(app, userController);

export default app;

Best Practices

✅ Do:

  • Keep domain layer pure - No external dependencies
  • Use interfaces (ports) - All external dependencies behind ports
  • Rich domain models - Entities with behavior, not just data
  • Use cases orchestrate - Don't put business logic in controllers
  • Inject dependencies - Constructor injection via DI container
  • Symbol-based tokens - Type-safe DI tokens
  • Scoped use cases - Per-request instances
  • Singleton repositories - Stateless, thread-safe
  • Result type - For expected failures (not exceptions)

❌ Don't:

  • Anemic domain models - Entities shouldn't be just data bags
  • Business logic in controllers - Controllers should be thin
  • Domain depending on infrastructure - Breaks dependency rule
  • Skip interfaces - Always use ports for external dependencies
  • Use concrete implementations in use cases - Depend on abstractions
  • Manual DI - Use the container
  • External DI libraries - Use custom container (InversifyJS, TSyringe)

Common Patterns

For complete error handling patterns (Result/Either types, Exception Hierarchy, Retry Logic, Circuit Breaker, Validation Strategies), see error-handling-patterns skill

Domain Events

// domain/events/user-created.event.ts
export class UserCreatedEvent {
  constructor(
    public readonly userId: string,
    public readonly email: string,
    public readonly occurredAt: Date = new Date()
  ) {}
}

// In Use Case
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
  // ... create user ...
  await this.eventBus.publish(new UserCreatedEvent(user.id.toString(), user.email.toString()));
  return response;
}

Remember

  • Clean Architecture is about maintainability, not perfection
  • The Dependency Rule is sacred - Always point inward
  • Domain is the core - Everything revolves around it
  • Test domain first - It's the most important part
  • Use custom DI container - No external libraries
  • Symbol-based tokens - Type-safe dependency injection
  • Scoped lifetimes for use cases - Per-request isolation