EpicenterHQ / services-layer
Install for your project team
Run this command in your project directory to install the skill for your entire team:
mkdir -p .claude/skills/services-layer && curl -L -o skill.zip "https://fastmcp.me/Skills/Download/2304" && unzip -o skill.zip -d .claude/skills/services-layer && rm skill.zip
Project Skills
This skill will be saved in .claude/skills/services-layer/ and checked into git. All team members will have access to it automatically.
Important: Please verify the skill by reviewing its instructions before using it.
Service layer patterns with createTaggedError, namespace exports, and Result types. Use when creating new services, defining domain-specific errors, or understanding the service architecture.
0 views
0 installs
Skill Content
---
name: services-layer
description: Service layer patterns with defineErrors, namespace exports, and Result types. Use when the user says "create a service", "service layer", or when creating new services, defining domain-specific errors, or understanding the service architecture.
metadata:
author: epicenter
version: '2.0'
---
# Services Layer Patterns
This skill documents how to implement services in the Whispering architecture. Services are pure, isolated business logic with no UI dependencies that return `Result<T, E>` types for error handling.
> **Related Skills**: See `error-handling` for trySync/tryAsync patterns. See `define-errors` for error variant factories. See `query-layer` for consuming services with TanStack Query.
## When to Apply This Skill
Use this pattern when you need to:
- Create a new service with domain-specific error handling
- Add error types with structured context (like HTTP status codes)
- Understand how services are organized and exported
- Implement platform-specific service variants (desktop vs web)
## Core Architecture
Services follow a three-layer architecture: **Service** → **Query** → **UI**
```
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ UI │ --> │ RPC/Query │ --> │ Services │
│ Components │ │ Layer │ │ (Pure) │
└─────────────┘ └─────────────┘ └──────────────┘
```
**Services are:**
- **Pure**: Accept explicit parameters, no hidden dependencies
- **Isolated**: No knowledge of UI state, settings, or reactive stores
- **Testable**: Easy to unit test with mock parameters
- **Consistent**: All return `Result<T, E>` types for uniform error handling
## Creating Errors with defineErrors
Every service defines domain-specific errors using `defineErrors` from wellcrafted. Errors are grouped into a namespace object where each key becomes a variant.
```typescript
import { defineErrors, type InferError, type InferErrors, extractErrorMessage } from 'wellcrafted/error';
import { Err, Ok, type Result, tryAsync, trySync } from 'wellcrafted/result';
// Namespace-style error definition — name describes the domain
const CompletionError = defineErrors({
ConnectionFailed: ({ cause }: { cause: unknown }) => ({
message: `Connection failed: ${extractErrorMessage(cause)}`,
cause,
}),
EmptyResponse: ({ providerLabel }: { providerLabel: string }) => ({
message: `${providerLabel} API returned an empty response`,
providerLabel,
}),
MissingParam: ({ param }: { param: string }) => ({
message: `${param} is required`,
param,
}),
});
// Type derivation — shadow the const with a type of the same name
type CompletionError = InferErrors<typeof CompletionError>;
type ConnectionFailedError = InferError<typeof CompletionError.ConnectionFailed>;
// Call sites — each variant returns Err<...> directly
return CompletionError.ConnectionFailed({ cause: error });
return CompletionError.EmptyResponse({ providerLabel: 'OpenAI' });
return CompletionError.MissingParam({ param: 'apiKey' });
```
### How defineErrors Works
`defineErrors({ ... })` takes an object of factory functions and returns a namespace object. Each key becomes a variant:
- **`name` is auto-stamped** from the key (e.g., key `NotFound` → `error.name === 'NotFound'`)
- **The factory function IS the message generator** — it returns `{ message, ...fields }`
- **Each variant returns `Err<...>` directly** — no separate `FooErr` constructor needed
- **Types use `InferError` / `InferErrors`** — not `ReturnType`
```typescript
// No-input variant (static message)
const RecorderError = defineErrors({
Busy: () => ({
message: 'A recording is already in progress',
}),
});
// Usage — no arguments needed
return RecorderError.Busy();
// Variant with derived fields — constructor extracts from raw input
const HttpError = defineErrors({
Response: ({ response, body }: { response: { status: number }; body: unknown }) => ({
message: `HTTP ${response.status}: ${extractErrorMessage(body)}`,
status: response.status,
body,
}),
});
// Usage — pass raw objects, constructor derives fields
return HttpError.Response({ response, body: await response.json() });
// error.message → "HTTP 401: Unauthorized"
// error.status → 401 (derived from response, flat on the object)
// error.name → "Response"
```
### Error Type Examples from the Codebase
```typescript
// Static message, no input needed
const RecorderError = defineErrors({
Busy: () => ({
message: 'A recording is already in progress',
}),
});
RecorderError.Busy()
// Multiple related errors in a single namespace
const HttpError = defineErrors({
Connection: ({ cause }: { cause: unknown }) => ({
message: `Failed to connect to the server: ${extractErrorMessage(cause)}`,
cause,
}),
Response: ({ response, body }: { response: { status: number }; body: unknown }) => ({
message: `HTTP ${response.status}: ${extractErrorMessage(body)}`,
status: response.status,
body,
}),
Parse: ({ cause }: { cause: unknown }) => ({
message: `Failed to parse response body: ${extractErrorMessage(cause)}`,
cause,
}),
});
// Union type for the whole namespace
type HttpError = InferErrors<typeof HttpError>;
// Individual variant type
type ConnectionError = InferError<typeof HttpError.Connection>;
```
## Key Rules
1. **Services never import settings** - Pass configuration as parameters
2. **Services never import UI code** - No toasts, no notifications, no WhisperingError
3. **Always return Result types** - Never throw errors
4. **Use trySync/tryAsync** - See the error-handling skill for details
5. **Export factory + Live instance** - Factory for testing, Live for production
6. **Use defineErrors namespaces** - Group related errors under a single namespace
7. **Derive types with InferError/InferErrors** - Not `ReturnType`
8. **Variant names describe the failure mode** - Never use generic names like `Service`, `Error`, or `Failed`. The namespace provides domain context (`RecorderError`), so the variant must say *what went wrong* (`AlreadyRecording`, `InitFailed`, `StreamAcquisition`). `RecorderError.Service` is meaningless — `RecorderError.AlreadyRecording` tells you exactly what happened.
9. **Split discriminated union inputs** - Each variant gets its own name and shape. If the constructor branches on its inputs (if/switch/ternary) to decide the message, each branch should be its own variant
10. **Transform cause in the constructor, not the call site** - Accept `cause: unknown` and call `extractErrorMessage(cause)` inside the factory's message template. Call sites pass the raw error: `{ cause: error }`. This centralizes message extraction where the message is composed and keeps call sites clean.
## References
Load these on demand based on what you're working on:
- If working with **error variant anti-patterns** (discriminated union inputs, branching constructors), read [references/error-anti-patterns.md](references/error-anti-patterns.md)
- If working with **service implementation details** (factory patterns, recorder service examples), read [references/service-implementation-pattern.md](references/service-implementation-pattern.md)
- If working with **service organization and platform variants** (namespace exports, desktop vs web services), read [references/service-organization-platforms.md](references/service-organization-platforms.md)
- If working with **error message authoring** (user-friendly/actionable message design), read [references/error-message-best-practices.md](references/error-message-best-practices.md)
- See `apps/whispering/src/lib/services/README.md` for architecture details
- See the `query-layer` skill for how services are consumed
- See the `error-handling` skill for trySync/tryAsync patterns