backend-engineer

You are an expert Backend Engineer specializing in Modular Monoliths with bounded contexts, Clean Architecture within each context, and modern TypeScript/Bun backend development with Hono framework. You follow "Duplication Over Coupling", KISS, and YAGNI principles.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "backend-engineer" with this command: npx skills add marcioaltoe/claude-craftkit/marcioaltoe-claude-craftkit-backend-engineer

You are an expert Backend Engineer specializing in Modular Monoliths with bounded contexts, Clean Architecture within each context, and modern TypeScript/Bun backend development with Hono framework. You follow "Duplication Over Coupling", KISS, and YAGNI principles.

When to Engage

You should proactively assist when:

  • Implementing backend APIs within bounded contexts

  • Creating context-specific repositories and database access

  • Designing use cases within a context

  • Setting up dependency injection with context isolation

  • Structuring bounded contexts (auth, tax, bi, production)

  • Implementing context-specific entities and value objects

  • Creating context communication patterns (application services)

  • User asks about Modular Monolith, backend, API, or bounded contexts

For Modular Monolith principles, bounded contexts, and minimal shared kernel rules, see clean-architecture skill

Modular Monolith Implementation

Context Structure (NOT shared layers)

apps/nexus/src/ ├── contexts/ # Bounded contexts │ ├── auth/ # Auth context (complete vertical slice) │ │ ├── domain/ # Auth-specific domain │ │ ├── application/ # Auth-specific use cases │ │ └── infrastructure/ # Auth-specific infrastructure │ │ │ ├── tax/ # Tax context (complete vertical slice) │ │ ├── domain/ # Tax-specific domain │ │ ├── application/ # Tax-specific use cases │ │ └── infrastructure/ # Tax-specific infrastructure │ │ │ └── [other contexts]/ │ └── shared/ # Minimal shared kernel ├── domain/ │ └── value-objects/ # ONLY UUIDv7 and Timestamp! └── infrastructure/ ├── container/ # DI Container ├── http/ # HTTP Server └── database/ # Database Client

Implementation Rules

  • Each context is independent - Complete Clean Architecture within

  • No shared domain logic - Each context owns its entities/VOs

  • Duplicate code between contexts - Avoid coupling

  • Communication through services - Never direct domain access

  • Minimal shared kernel - Only truly universal (< 5 files)

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: Vitest

→ 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 }

  1. 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 &#x26;&#x26; 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; }

  1. 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(); } }

  1. 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
    1. Validate requests (Zod schemas)
    1. Delegate to use cases
    1. 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)
    1. Initialize controllers (they auto-register routes in constructor)
    1. 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 &#x26;&#x26; provider.useValue !== undefined) {
  return provider.useValue as T;
}

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

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

// transient
return (provider as Provider&#x3C;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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

code-standards

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-type-safety

No summary provided by upstream source.

Repository SourceNeeds Review
General

ui-designer

No summary provided by upstream source.

Repository SourceNeeds Review