Component architecture
Skill batur/ts-baseline-docs/.agents/skills/component-architecture
TypeScript software architecture baseline with VS Code Agent Skills for AI coding, review, security, testing, API design, and delivery standards.
npx -y skills add batur/ts-baseline-docs --skill component-architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Enforce the TypeScript component-based architecture baseline. Use when creating, moving, or reviewing modules, features, shared code, use-cases, controllers, routes, repositories, adapters, serializers, integrations, dependency boundaries, or cross-component imports.
SKILL.md
12.4 KB, as published. Nobody here has run it
Component Architecture
Goal
Use this skill to keep TypeScript projects organized around business capabilities and explicit boundaries.
A component is an ownership and dependency boundary, not just a folder. Business policy must stay at the center. Frameworks, databases, API clients, queues, caches, LLM providers, Supabase, Firebase, MongoDB, Stripe, Slack, OpenAI, and other external systems are details that must not control business rules.
Default to simple, explicit structure. Do not create unnecessary abstractions or empty architecture folders before complexity requires them.
When to use this skill
Use this skill when a task involves any of these actions:
- Add or change a backend business module.
- Add or change a frontend feature.
- Move code between folders or components.
- Create or review a controller, route, use-case, policy, repository, adapter, serializer, API client, schema, or shared utility.
- Add an external integration or provider SDK.
- Decide whether code belongs in
shared/. - Review imports across
modules/,features/,shared/, orapp/. - Review AI-generated code for architectural boundary violations.
- Decide whether to introduce
domain/,application/,infrastructure/, orpresentation/folders.
Canonical folder model
Backend default
src/
app/
bootstrap.ts
container.ts
server.ts
modules/
users/
index.ts
user.schema.ts
user.types.ts
user.errors.ts
user.repository.ts
create-user.use-case.ts
update-user.use-case.ts
user.serializer.ts
user.test.ts
shared/
config/
server-env.ts
app-config.ts
errors/
app-error.ts
error-code.ts
http-error.mapper.ts
logger/
logger.ts
validation/
zod-error.mapper.ts
http/
pagination.ts
main.ts
Frontend default
src/
app/
providers/
router.tsx
app.tsx
features/
auth/
index.ts
auth.schema.ts
auth.types.ts
login-form.tsx
use-login.ts
auth.api.ts
projects/
index.ts
project.schema.ts
project.types.ts
project-list.tsx
project-card.tsx
use-projects.ts
projects.api.ts
shared/
ui/
button.tsx
dialog.tsx
config/
client-env.ts
errors/
http/
validation/
main.tsx
Component types
Use these component categories:
- Business components:
users,organizations,billing,projects,conversations. - Frontend feature components:
auth,projects,settings,dashboard. - Integration components:
stripe,slack,openai,gmail,supabase,firebase. - Shared technical components:
logger,config,errors,validation,http,security,date,strings.
Do not organize business code only by technical layers such as global services/, repositories/,
controllers/, or types/ folders. Prefer capability ownership.
Public and private boundaries
Each component exposes its public API through index.ts.
Files not exported from index.ts are internal by default.
Cross-component imports must go through the target component public API.
// Good
import { createUser } from "@/modules/users/index.js";
// Bad
import { createUser } from "@/modules/users/create-user.use-case.js";
Same-component internal files may use relative imports.
// Allowed inside src/modules/users/update-user.use-case.ts
import { USER_ERROR_CODE } from "./user.errors.js";
Dependency direction
Use this dependency direction:
presentation -> application -> domain
infrastructure -> application/domain contracts
shared -> no business modules
app -> composition root and wiring
Rules:
- Controllers/routes stay thin.
- Use-cases/application services coordinate business flow.
- Domain logic must not import framework, database, SDK, HTTP, queue, or provider details.
- Infrastructure implements contracts and may import provider/database SDKs.
shared/must not import business modules.app/wires dependencies, reads config, starts servers, and builds composition roots.
Flat first, layered when needed
Start flat inside a component.
modules/users/
index.ts
user.schema.ts
user.types.ts
user.repository.ts
drizzle-user.repository.ts
create-user.use-case.ts
user.serializer.ts
Introduce explicit folders only when complexity requires them.
modules/users/
domain/
user.ts
user.errors.ts
application/
create-user.use-case.ts
user.repository.ts
infrastructure/
drizzle-user.repository.ts
user.mapper.ts
presentation/
user.serializer.ts
user.routes.ts
Do not create empty architecture folders just to look clean. KISS and YAGNI apply.
Backend rules
When creating or reviewing backend code:
- Routes/controllers validate boundary input, call use-cases, and return serialized responses.
- Request body, query, path params, webhooks, provider responses, and AI outputs are external inputs and must be validated at the boundary.
- Use-cases carry business flow and authorization checks.
- Repositories hide persistence details.
- Adapters hide external providers.
- Serializers produce API DTOs.
- Database rows must not be returned directly from API handlers.
- Domain/application code must not import Drizzle, Prisma, Supabase JS, Firebase SDK, MongoDB driver, OpenAI SDK, Stripe SDK, Slack SDK, or similar provider details.
Frontend rules
When creating or reviewing frontend code:
- Use
features/for feature-owned UI, hooks, schemas, and API calls. - Use
shared/uionly for generic, domain-independent UI components. - Feature-specific UI belongs inside the feature, not
shared/ui. - A feature must not deep-import internals from another feature.
- API wrappers for a feature live inside that feature unless they are truly generic HTTP infrastructure.
- UI components should not perform hidden API response-to-domain transformations; use feature API/mapper/serializer boundaries.
Shared code rules
Put code in shared/ only when it is:
- Used by multiple components, and
- Domain-independent, or
- A technical standard/helper.
Good shared folders:
shared/config
shared/errors
shared/http
shared/logger
shared/security
shared/validation
shared/date
shared/strings
shared/ui
Avoid:
shared/utils/misc.ts
shared/helpers.ts
shared/services/
shared/repositories/
shared/types.ts
Do not move code into shared/ just because a second caller appears once. Prefer duplication over
premature abstraction when the domain meaning is not yet stable.
External integration rules
External systems are details. Keep them behind adapters.
Good:
modules/billing/
application/
billing-provider.ts
create-checkout-session.use-case.ts
infrastructure/
stripe-billing-provider.ts
Bad:
// In a use-case or domain file
import Stripe from "stripe";
Provider SDKs may live in infrastructure/adapters, not in domain/application logic.
Repository and persistence rules
Use repository/query modules when persistence access is part of business/application logic.
- Repository interfaces belong to the application/domain side when abstraction is needed.
- Implementations belong to infrastructure/persistence.
- Related writes must use explicit transactions.
- Tenant-scoped queries must include
organizationIdortenantId. - Prefer methods like
findByIdForOrganization(id, organizationId)over genericfindById(id)for tenant-scoped resources. - Raw SQL requires written justification, explicit review, parameterization, and tenant filtering when applicable.
Serializer and DTO rules
API response shape must not be dictated by database rows, ORM models, provider objects, or domain internals.
Use serializers:
export function serializeUser(user: User): UserResponseDto {
return {
id: user.id,
email: user.email,
displayName: user.displayName,
createdAt: user.createdAt.toISOString(),
};
}
Do not return database rows directly.
Naming and file rules
Follow the baseline naming rules:
- File names and folder names use
kebab-case. - Classes and types use
PascalCase. - Variables and functions use
camelCase. - Constants use
SCREAMING_SNAKE_CASE. - Interfaces do not use the
Iprefix. - Application modules use named exports by default.
- Default exports are allowed only when required by framework/tooling conventions.
Coding workflow
When adding or changing code:
- Identify the owning component or feature.
- Decide whether the code is business, presentation, infrastructure, integration, or shared technical code.
- Place the file in the smallest correct ownership boundary.
- Keep controller/route code thin.
- Put business flow in a use-case/application service.
- Keep SDK/database/framework details behind adapters or repositories.
- Export only public component API from
index.ts. - Use cross-component imports only through public APIs.
- Add or update tests for behavior, boundary, tenant, and authorization changes.
- Update docs/ADR when architectural boundaries change.
Review checklist
Use this checklist when reviewing code:
- Is the file in the correct component/feature/shared boundary?
- Did the code introduce a global
services/,helpers/,repositories/, ortypes/dumping ground? - Does any component import another component's internal files?
- Does
shared/import business modules? - Are controllers/routes thin?
- Is business logic in use-cases/application/domain instead of controllers, UI, repositories, or SDK adapters?
- Does domain/application import provider, database, framework, queue, cache, or SDK details?
- Are external integrations hidden behind adapters?
- Are repository/query functions tenant-safe where needed?
- Are database rows/provider responses serialized before API output?
- Was code moved into
shared/prematurely? - Were tests added or updated for changed behavior?
- Does this change require docs or ADR updates?
Anti-patterns
Avoid these patterns:
shared/utils/misc.tshelpers.tsas a dumping ground- global
services/ - global
repositories/ - global
types.ts - controller business rules
- UI components transforming API contracts into business policy
- repositories enforcing business authorization
- domain/application importing SDKs
- cross-module deep imports
- automatic
index.tsbarrels everywhere without public API intent - empty
domain/application/infrastructure/presentationfolders with no need - circular dependencies
Good examples
Thin controller
export async function updateUserController(request: Request) {
const authContext = await AUTH_GUARD.requireUser(request);
const input = UPDATE_USER_SCHEMA.parse(await request.json());
const user = await updateUser({
authContext,
input,
userId: request.params.userId,
});
return {
data: serializeUser(user),
};
}
Use-case with authorization and repository boundary
export async function updateProject(params: UpdateProjectParams): Promise<Project> {
const project = await PROJECT_REPOSITORY.findByIdForOrganization(
params.projectId,
params.authContext.organizationId,
);
if (!project) {
throw new AppError(PROJECT_ERROR_CODE.PROJECT_NOT_FOUND, "Project not found.", 404);
}
await PERMISSIONS.require(params.authContext, "project:update", {
organizationId: project.organizationId,
});
return PROJECT_REPOSITORY.update(project.id, params.input);
}
Do not
Do not:
- Add a new architecture layer before a real complexity exists.
- Put domain-specific code into
shared/for convenience. - Import provider/database SDKs in domain or application code.
- Return raw ORM rows or provider responses from API routes.
- Skip authorization because the UI hides the action.
- Bypass component public APIs with deep imports.
- Treat ESLint boundary rules as the only architecture enforcement; also use code review.