service-layer-style

Enforces ServiceCtx-based service-layer patterns for T3 monorepos with tRPC and Drizzle.

Updated Aug 2, 2026
One-click install
npx skills add https://github.com/leonardoacosta/skills --skill service-layer-style-leonardoacosta
Or copy as Structured Prompt for Agent
Please help me install this Agent Skill.
Skill: service-layer-style
Source: https://github.com/leonardoacosta/skills/tree/main/t3-stack-kit/skills/service-layer-style
Command: npx skills add https://github.com/leonardoacosta/skills --skill service-layer-style-leonardoacosta

SYSTEM DOCUMENTATION & REQUIREMENTS

What problem does it solve? Service layers in T3 Turbo monorepos often accumulate class-based BaseService patterns with this.x! non-null assertions, leaked $inferSelect return types, and silent-swallowed errors that hide bugs from both users and observability tooling. This Skill provides a portable style guide that keeps the packages/api/src/services/** layer typesafe, testable, and observable. ## Core Features & Use Cases - ServiceCtx Invariant: Defines a single context type (db, scoped logger, userId, eventSeriesId, correlationId, abortSignal) threaded as the first argument to every service function, replacing hidden globals and constructor injection. - Four-Style Decision Tree: Guides selection between pure functions, factory + closure, namespace factory families, and type-routed generics based on shared-implementation ratios. - Forbidden Patterns & Migration Checklist: Enumerates anti-patterns (BaseService inheritance, $inferSelect exports, silent catches, as any, double Sentry capture) with a 12-step per-service migration checklist. - Use Case: When reviewing a PR that adds a new checkout service for attendee, vendor, and sponsor flows, use this Skill to decide between a namespace factory family and type-routed generics, then verify the code avoids the anti-exemplar patterns. ## Quick Start Ask the agent to review or scaffold a service in packages/api/src/services following the service-layer-style guide, choosing the appropriate style and threading ServiceCtx as the first argument.

Frequently Asked Questions about service-layer-style

High-intent search queries and answers about installing and using this skill.

FAQPage Schema
How do I structure a service layer in a T3 Turbo monorepo?

Thread a ServiceCtx object (db, scoped logger, userId, eventSeriesId, correlationId) as the first argument to every service function. Pick pure functions for 1-2 exports, a factory with closure for 3+ methods sharing setup, and escalate to namespace families or generics only when specialization ratios justify it.

When should I use a factory function versus a class for services?

Use a factory function returning an object literal whenever multiple methods share setup like a scoped logger, Stripe client, or OTel span. Classes that extend BaseService are forbidden because this.db! non-null assertions hide runtime failures from the type system.

Why should services not return Drizzle $inferSelect types?

Exporting $inferSelect leaks every persistence column, including deletedAt and internal fields, into the public API, making future column removals breaking changes. Define an explicit DTO type and a toDto() mapper as the single place controlling what leaves the service.

How do I test tRPC services without mocking the database module?

Build a fake ServiceCtx by hand with a fake db and silent logger, then pass it directly to the service function. The ctx parameter is the test seam, so no jest.mock or vi.mock calls on db or logger modules are needed.

How should errors be handled in a tRPC service layer?

Throw DomainError with code, userMessage, and retryable fields, and let the tRPC error pipeline capture it at the outer boundary. Never silently catch and return empty arrays, and never call Sentry.captureException before throwing, which creates duplicate issues.

Where does transaction management go in a service layer?

Run transactions at the service boundary inside the factory method using ctx.db.transaction. Create a fresh ServiceCtx with the transaction client as db and pass it to nested service calls so everything stays inside the transaction.