Claude-skill-registry app-architecture
Create apps following contract-port architecture with composition roots. Use when creating new apps in apps/, scaffolding CLI tools, setting up dependency injection, or when the user asks about app structure, entrypoints, or platform-agnostic design.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/app-architecture" ~/.claude/skills/majiayu000-claude-skill-registry-app-architecture && rm -rf "$T"
manifest:
skills/data/app-architecture/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
App Architecture
Apps mirror the package architecture - they should be platform-agnostic outside of the entrypoint where ports are assembled with platform-specific globals.
Core Principle
Assemble all ports at the top level (composition root), then feed them down to individual modules.
┌─────────────────────────────────────────────────────────────┐ │ Entry Point (cli.ts) │ │ Parse args, call composition root │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Composition Root │ │ (composition.ts) │ │ Inject platform globals → Create ports → Return │ └─────────────────────────────────────────────────────────────┘ │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Module A │ │ Module B │ │ Module C │ │ (no globals)│ │ (no globals)│ │ (no globals)│ └─────────────┘ └─────────────┘ └─────────────┘
File Structure
apps/{app-name}/ ├── src/ │ ├── cli.ts # Entry point (thin) - only parses args, calls composition │ ├── composition.ts # Composition root - assembles all ports │ ├── types.ts # App-specific types │ ├── errors.ts # Error classes (extend ConveauxError) │ └── {domain}.ts # Domain modules - platform-agnostic ├── CLAUDE.md # Inline instructions for this app ├── README.md # User documentation ├── package.json └── tsconfig.json
Composition Root Pattern
The composition root is the ONLY place where platform globals appear:
// composition.ts import type { Env } from '@scope/contract-env'; import type { Logger } from '@scope/contract-logger'; import type { WallClock } from '@scope/contract-wall-clock'; import type { EphemeralScheduler } from '@scope/contract-ephemeral-scheduler'; import { createEnv, createShellEnvSource, createStaticEnvSource } from '@scope/port-env'; import { createEphemeralScheduler } from '@scope/port-ephemeral-scheduler'; import { createLogger, createJsonFormatter, createPrettyFormatter } from '@scope/port-logger'; import { createOutChannel } from '@scope/port-outchannel'; import { createWallClock } from '@scope/port-wall-clock'; // Define what deps the app needs export interface RuntimeDeps { readonly logger: Logger; readonly clock: WallClock; readonly env: Env; readonly scheduler: EphemeralScheduler; } export interface RuntimeOptions { readonly json?: boolean; readonly verbose?: boolean; } // Factory that creates all deps - platform globals only appear HERE export function createRuntimeDeps(options: RuntimeOptions = {}): RuntimeDeps { // Inject platform globals into ports const clock = createWallClock({ Date }); const scheduler = createEphemeralScheduler({ setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, setInterval: globalThis.setInterval, clearInterval: globalThis.clearInterval, }); const logChannel = createOutChannel(process.stderr); const formatter = options.json ? createJsonFormatter() : createPrettyFormatter({ colors: process.stderr.isTTY ?? false }); const logger = createLogger({ Date, channel: logChannel, clock, options: { formatter, minLevel: options.verbose ? 'debug' : 'info' }, }); const env = createEnv({ sources: [ createShellEnvSource( { getEnv: (name) => process.env[name] }, { name: 'shell', priority: 100 } ), createStaticEnvSource( { DEFAULT_TIMEOUT: '30' }, { name: 'defaults', priority: 0 } ), ], }); return { logger, clock, env, scheduler }; }
Entry Point Pattern
The entry point should be thin - just parse args and call composition:
// cli.ts #!/usr/bin/env node import type { Logger } from '@scope/contract-logger'; import { Command } from 'commander'; import { createRuntimeDeps } from './composition.js'; import { runApp } from './app.js'; import { createClient } from './client.js'; const program = new Command(); program .name('my-app') .action(async (options: CliOptions) => { // 1. Create all deps at the top level const deps = createRuntimeDeps({ json: options.json, verbose: options.verbose, }); const { logger, clock, env, scheduler } = deps; // 2. Create app-specific services, passing deps down const client = createClient({ logger, clock }); // 3. Run the app, passing deps down const result = await runApp({ logger, clock, client, scheduler }, config); console.log(JSON.stringify(result, null, 2)); process.exit(EXIT_CODES[result.status]); }); program.parse();
Domain Module Pattern
Domain modules receive deps via injection - they never use globals:
// client.ts import type { Logger } from '@scope/contract-logger'; import type { WallClock } from '@scope/contract-wall-clock'; // Define what deps THIS module needs (subset of RuntimeDeps) export interface ClientDeps { readonly logger: Logger; readonly clock: WallClock; } // Interface for what this module provides export interface Client { fetch(url: string): Promise<Response>; } // Factory receives deps, returns implementation export function createClient(deps: ClientDeps): Client { const { logger, clock } = deps; return { async fetch(url: string) { const startTime = clock.nowMs(); // Use injected clock, not Date.now() logger.debug('Fetching', { url }); // ... implementation const durationMs = clock.nowMs() - startTime; logger.debug('Fetched', { url, durationMs }); return response; } }; }
Checklist for Creating Apps
Composition Root
- Single
file that creates all runtime depscomposition.ts - Platform globals (Date, setTimeout, process.env) only appear here
- Exports
interface andRuntimeDeps
factorycreateRuntimeDeps - Options (json, verbose) are separate from deps
Entry Point
- Thin
- only arg parsing and wiringcli.ts - Calls
once at the topcreateRuntimeDeps() - Passes deps down to all modules
- Handles errors with proper exit codes
Domain Modules
- Each module defines its own
interface*Deps - Deps interface only includes contracts the module needs
- No direct use of globals (Date.now, console, process.env)
- Factory pattern:
createX(deps): X
Types
-
for shared app-specific typestypes.ts - Config types separate from deps types
- Exit codes as const object
Errors
-
with classes extendingerrors.ts
orUserErrorRetryableError - Actionable error messages with guidance
- Use
for proper exit codesgetExitCode()
Common Deps by Use Case
| Need | Contract | Port |
|---|---|---|
| Current time | | |
| Logging | | |
| Environment vars | | |
| Delays/timers | | |
| Writing output | | |
| Error handling | | |
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
in module | Hidden platform dependency | Inject |
in module | Untestable timing | Inject |
in module | Hidden env dependency | Inject |
in module | Hidden output dependency | Inject |
| Deps created inside module | Can't test with mocks | Pass deps from composition root |
| Module imports port directly | Bypasses DI | Import contract types only |