wellcrafted 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,691 +5,324 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/wellcrafted)](https://bundlephobia.com/package/wellcrafted)
7
7
 
8
- *Delightful TypeScript utilities for elegant, type-safe applications*
8
+ *Define your errors. Type the rest.*
9
9
 
10
- ## Transform unpredictable errors into type-safe results
10
+ Tagged errors and Result types as plain objects. < 2KB, zero dependencies.
11
11
 
12
- ```typescript
13
- // ❌ Before: Which errors can this throw? 🤷
14
- try {
15
- await saveUser(user);
16
- } catch (error) {
17
- // ... good luck debugging in production
18
- }
19
-
20
- // ✅ After: Every error is visible and typed
21
- const { data, error } = await saveUser(user);
22
- if (error) {
23
- switch (error.name) {
24
- case "ValidationError":
25
- showToast(`Invalid ${error.field}`);
26
- break;
27
- case "AuthError":
28
- redirectToLogin();
29
- break;
30
- // TypeScript ensures you handle all cases!
31
- }
32
- }
33
- ```
34
-
35
- ## A collection of simple, powerful primitives
36
-
37
- ### 🎯 Result Type
38
- Make errors explicit in function signatures
39
- ```typescript
40
- function divide(a: number, b: number): Result<number, string> {
41
- if (b === 0) return Err("Division by zero");
42
- return Ok(a / b);
43
- }
44
- ```
45
-
46
- ### 🏷️ Brand Types
47
- Create distinct types from primitives
48
- ```typescript
49
- type UserId = string & Brand<"UserId">;
50
- type OrderId = string & Brand<"OrderId">;
12
+ Most Result libraries hand you a container and leave the error type as an exercise. You get `Ok` and `Err` but nothing to help you define, compose, or serialize the errors themselves. So you end up with string literals, ad-hoc objects, or class hierarchies that break the moment you call `JSON.stringify`.
51
13
 
52
- // TypeScript prevents mixing them up!
53
- function getUser(id: UserId) { /* ... */ }
54
- ```
14
+ wellcrafted takes the opposite approach: start with the errors. `defineErrors` gives you typed, serializable, composable error variants inspired by Rust's [thiserror](https://docs.rs/thiserror). The Result type is just `{ data, error }` destructuring — the same shape you already know from Supabase, SvelteKit load functions, and TanStack Query. No `.isOk()` method chains, no `.map().andThen().orElse()` pipelines. Check `error`, use `data`. That's it.
55
15
 
56
- ### 📋 Tagged Errors
57
- Structured, serializable errors with a declarative API
58
16
  ```typescript
59
- import { defineErrors, type InferError } from "wellcrafted/error";
60
-
61
- const errors = defineErrors({
62
- // Static errorno fields needed
63
- ValidationError: () => ({
64
- message: "Email is required",
17
+ import { defineErrors, extractErrorMessage, type InferErrors } from "wellcrafted/error";
18
+ import { tryAsync, Ok, type Result } from "wellcrafted/result";
19
+
20
+ // Define domain errors all variants in one call
21
+ const UserError = defineErrors({
22
+ AlreadyExists: ({ email }: { email: string }) => ({
23
+ message: `User ${email} already exists`,
24
+ email,
65
25
  }),
66
- // Structured error fields are spread flat on the error object
67
- ApiError: (fields: { endpoint: string }) => ({
68
- ...fields,
69
- message: `Request to ${fields.endpoint} failed`,
26
+ CreateFailed: ({ email, cause }: { email: string; cause: unknown }) => ({
27
+ message: `Failed to create user ${email}: ${extractErrorMessage(cause)}`,
28
+ email,
29
+ cause,
70
30
  }),
71
31
  });
72
- const { ValidationError, ApiError } = errors;
73
- ```
74
-
75
- ### 🔄 Query Integration
76
- Seamless TanStack Query integration with dual interfaces
77
- ```typescript
78
- import { createQueryFactories } from "wellcrafted/query";
79
- import { QueryClient } from "@tanstack/query-core";
80
-
81
- const queryClient = new QueryClient();
82
- const { defineQuery, defineMutation } = createQueryFactories(queryClient);
83
-
84
- // Define operations that return Result types
85
- const userQuery = defineQuery({
86
- queryKey: ['users', userId],
87
- queryFn: () => getUserFromAPI(userId) // Returns Result<User, ApiError>
88
- });
89
-
90
- // Use reactively in components with automatic state management
91
- // Svelte 5 requires accessor function; React uses options directly
92
- const query = createQuery(() => userQuery.options); // Svelte
93
- // query.data, query.error, query.isPending all managed automatically
32
+ type UserError = InferErrors<typeof UserError>;
33
+ // ^? { name: "AlreadyExists"; message: string; email: string }
34
+ // | { name: "CreateFailed"; message: string; email: string; cause: unknown }
35
+
36
+ // Each factory returns Err<...> directly — no wrapping needed
37
+ async function createUser(email: string): Promise<Result<User, UserError>> {
38
+ const existing = await db.findByEmail(email);
39
+ if (existing) return UserError.AlreadyExists({ email });
40
+
41
+ return tryAsync({
42
+ try: () => db.users.create({ email }),
43
+ catch: (error) => UserError.CreateFailed({ email, cause: error }),
44
+ });
45
+ }
94
46
 
95
- // Or use imperatively for direct execution (perfect for event handlers)
96
- const { data, error } = await userQuery.fetch();
97
- // Or shorthand: await userQuery() (same as .ensure())
47
+ // Discriminate with switch TypeScript narrows automatically
48
+ const { data, error } = await createUser("alice@example.com");
98
49
  if (error) {
99
- showErrorToast(error.message);
100
- return;
50
+ switch (error.name) {
51
+ case "AlreadyExists": console.log(error.email); break;
52
+ case "CreateFailed": console.log(error.email); break;
53
+ // ^ TypeScript knows exactly which fields exist
54
+ }
101
55
  }
102
- // Use data...
103
56
  ```
104
57
 
105
- ## Installation
58
+ ## Install
106
59
 
107
60
  ```bash
108
61
  npm install wellcrafted
109
62
  ```
110
63
 
111
- ## Quick Start
112
-
113
- ```typescript
114
- import { tryAsync } from "wellcrafted/result";
115
- import { defineErrors, type InferError } from "wellcrafted/error";
116
-
117
- // Define your errors declaratively
118
- const errors = defineErrors({
119
- ApiError: (fields: { endpoint: string }) => ({
120
- ...fields,
121
- message: `Failed to fetch ${fields.endpoint}`,
122
- }),
123
- });
124
- const { ApiError, ApiErr } = errors;
125
- type ApiError = InferError<typeof errors, "ApiError">;
126
-
127
- // Wrap any throwing operation
128
- const { data, error } = await tryAsync({
129
- try: () => fetch('/api/user').then(r => r.json()),
130
- catch: (e) => ApiErr({ endpoint: '/api/user' })
131
- });
132
-
133
- if (error) {
134
- console.error(`${error.name}: ${error.message}`);
135
- } else {
136
- console.log("User:", data);
137
- }
138
- ```
139
-
140
- ## Core Features
141
-
142
- <table>
143
- <tr>
144
- <td>
145
-
146
- **🎯 Explicit Error Handling**
147
- All errors visible in function signatures
148
-
149
- </td>
150
- <td>
151
-
152
- **📦 Serialization-Safe**
153
- Plain objects work everywhere
154
-
155
- </td>
156
- <td>
157
-
158
- **✨ Elegant API**
159
- Clean, intuitive patterns
160
-
161
- </td>
162
- </tr>
163
- <tr>
164
- <td>
165
-
166
- **🔍 Zero Magic**
167
- ~50 lines of core code
168
-
169
- </td>
170
- <td>
171
-
172
- **🚀 Lightweight**
173
- Zero dependencies, < 2KB
174
-
175
- </td>
176
- <td>
177
-
178
- **🎨 Composable**
179
- Mix and match utilities
180
-
181
- </td>
182
- </tr>
183
- </table>
184
-
185
- ## The Result Pattern Explained
186
-
187
- The Result type makes error handling explicit and type-safe:
188
-
189
- ```typescript
190
- type Ok<T> = { data: T; error: null };
191
- type Err<E> = { error: E; data: null };
192
- type Result<T, E> = Ok<T> | Err<E>;
193
- ```
194
-
195
- This creates a discriminated union where TypeScript automatically narrows types:
64
+ ## Why define errors at all?
196
65
 
197
- ```typescript
198
- if (result.error) {
199
- // TypeScript knows: error is E, data is null
200
- } else {
201
- // TypeScript knows: data is T, error is null
202
- }
203
- ```
66
+ You can use `Ok` and `Err` with any value. So why bother with `defineErrors`?
204
67
 
205
- ## Basic Patterns
68
+ Because in practice, errors aren't random. Every service has a handful of things that can go wrong, and you want to enumerate them upfront. A user service has `AlreadyExists`, `CreateFailed`, `InvalidEmail`. An HTTP client has `Connection`, `Timeout`, `Response`. These are logical groups — the error vocabulary for a domain. Rust codified this with [thiserror](https://docs.rs/thiserror). `defineErrors` brings the same pattern to TypeScript, but outputs plain objects instead of classes.
206
69
 
207
- ### Handle Results with Destructuring
70
+ **Errors are data, not classes.** Plain frozen objects with no prototype chain. `JSON.stringify` just works — no `stack` property eating up your logs, no `instanceof` checks that break across package boundaries. This matters anywhere errors cross a serialization boundary: Web Workers, server actions, sync engines, IPC. The error you create is the error that arrives.
208
71
 
209
- ```typescript
210
- const { data, error } = await someOperation();
72
+ **Every factory returns `Err<...>` directly.** No wrapping step. Return it from a `tryAsync` catch handler or as a standalone early return — `if (existing) return UserError.AlreadyExists({ email })`. The Result type flows naturally.
211
73
 
212
- if (error) {
213
- // Handle error with full type safety
214
- return;
215
- }
74
+ **Discriminated unions for free.** `switch (error.name)` gives you full TypeScript narrowing. No `instanceof`, no type predicates, no manual union types. Add a new variant and every consumer that switches gets a compile error until they handle it.
216
75
 
217
- // Use data - TypeScript knows it's safe
218
- ```
76
+ ## Wrapping unsafe code
219
77
 
220
- ### Wrap Unsafe Operations
78
+ `trySync` and `tryAsync` turn throwing operations into `Result` types. The `catch` handler receives the raw error and returns an `Err<...>` from your `defineErrors` factories.
221
79
 
222
80
  ```typescript
223
- import { defineErrors, type InferError } from "wellcrafted/error";
81
+ import { trySync, tryAsync } from "wellcrafted/result";
224
82
 
225
- // Define errors declaratively
226
- const errors = defineErrors({
227
- ParseError: (fields: { input: string }) => ({
228
- ...fields,
229
- message: `Invalid JSON: ${fields.input.slice(0, 50)}`,
230
- }),
231
- NetworkError: (fields: { url: string }) => ({
232
- ...fields,
233
- message: `Request to ${fields.url} failed`,
83
+ const JsonError = defineErrors({
84
+ ParseFailed: ({ input, cause }: { input: string; cause: unknown }) => ({
85
+ message: `Invalid JSON: ${extractErrorMessage(cause)}`,
86
+ input: input.slice(0, 100),
87
+ cause,
234
88
  }),
235
89
  });
236
- const { ParseErr, NetworkErr } = errors;
237
90
 
238
91
  // Synchronous
239
- const result = trySync({
240
- try: () => JSON.parse(jsonString),
241
- catch: () => ParseErr({ input: jsonString })
92
+ const { data, error } = trySync({
93
+ try: () => JSON.parse(rawInput),
94
+ catch: (cause) => JsonError.ParseFailed({ input: rawInput, cause }),
242
95
  });
243
96
 
244
97
  // Asynchronous
245
- const result = await tryAsync({
246
- try: () => fetch(url),
247
- catch: () => NetworkErr({ url })
98
+ const { data, error } = await tryAsync({
99
+ try: () => fetch(url).then((r) => r.json()),
100
+ catch: (cause) => HttpError.Connection({ url, cause }),
248
101
  });
249
102
  ```
250
103
 
251
- ### Real-World Service + Query Layer Example
104
+ When `catch` returns `Ok(fallback)` instead of `Err`, the return type narrows to `Ok<T>` — no error checking needed:
252
105
 
253
106
  ```typescript
254
- // 1. Service Layer - Pure business logic
255
- import { defineErrors, type InferError } from "wellcrafted/error";
256
- import { tryAsync, Result, Ok } from "wellcrafted/result";
257
-
258
- const recorderErrors = defineErrors({
259
- RecorderServiceError: (fields: { currentState?: string; permissions?: string }) => ({
260
- ...fields,
261
- message: fields.permissions
262
- ? `Missing ${fields.permissions} permission`
263
- : `Invalid recorder state: ${fields.currentState}`,
264
- }),
107
+ const { data: parsed } = trySync({
108
+ try: (): unknown => JSON.parse(riskyJson),
109
+ catch: () => Ok([]),
265
110
  });
266
- const { RecorderServiceError, RecorderServiceErr } = recorderErrors;
267
- type RecorderServiceError = InferError<typeof recorderErrors, "RecorderServiceError">;
268
-
269
- export function createRecorderService() {
270
- let isRecording = false;
271
- let currentBlob: Blob | null = null;
272
-
273
- return {
274
- async startRecording(): Promise<Result<void, RecorderServiceError>> {
275
- if (isRecording) {
276
- return RecorderServiceErr({ currentState: 'recording' });
277
- }
278
-
279
- return tryAsync({
280
- try: async () => {
281
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
282
- const recorder = new MediaRecorder(stream);
283
- // ... recording setup
284
- isRecording = true;
285
- },
286
- catch: () => RecorderServiceErr({ permissions: 'microphone' })
287
- });
288
- },
289
-
290
- async stopRecording(): Promise<Result<Blob, RecorderServiceError>> {
291
- if (!isRecording) {
292
- return RecorderServiceErr({ currentState: 'idle' });
293
- }
294
-
295
- // Stop recording and return blob...
296
- isRecording = false;
297
- return Ok(currentBlob!);
298
- }
299
- };
300
- }
111
+ // parsed is always defined the catch recovered
112
+ ```
301
113
 
302
- // 2. Query Layer - Adds caching, reactivity, and UI error handling
303
- import { createQueryFactories } from "wellcrafted/query";
114
+ ## Composing errors across layers
304
115
 
305
- const { defineQuery, defineMutation } = createQueryFactories(queryClient);
116
+ This is where the pattern pays off. Each layer defines its own error vocabulary; inner errors become `cause` fields, and `extractErrorMessage` formats them inside the factory so call sites stay clean.
306
117
 
307
- export const recorder = {
308
- getRecorderState: defineQuery({
309
- queryKey: ['recorder', 'state'],
310
- queryFn: async () => {
311
- const { data, error } = await services.recorder.getState();
312
- if (error) {
313
- // Transform service error to UI-friendly error
314
- return Err({
315
- title: "❌ Failed to get recorder state",
316
- description: error.message,
317
- action: { type: 'retry' }
318
- });
319
- }
320
- return Ok(data);
321
- },
322
- refetchInterval: 1000, // Poll for state changes
118
+ ```typescript
119
+ // Service layer: domain errors wrap raw failures via cause
120
+ const UserServiceError = defineErrors({
121
+ NotFound: ({ userId }: { userId: string }) => ({
122
+ message: `User ${userId} not found`,
123
+ userId,
323
124
  }),
125
+ FetchFailed: ({ userId, cause }: { userId: string; cause: unknown }) => ({
126
+ message: `Failed to fetch user ${userId}: ${extractErrorMessage(cause)}`,
127
+ userId,
128
+ cause,
129
+ }),
130
+ });
131
+ type UserServiceError = InferErrors<typeof UserServiceError>;
132
+
133
+ async function getUser(userId: string): Promise<Result<User, UserServiceError>> {
134
+ const response = await tryAsync({
135
+ try: () => fetch(`/api/users/${userId}`),
136
+ catch: (cause) => UserServiceError.FetchFailed({ userId, cause }),
137
+ // raw fetch error becomes cause ^^^^^
138
+ });
139
+ if (response.error) return response;
140
+
141
+ if (response.data.status === 404) return UserServiceError.NotFound({ userId });
142
+
143
+ return tryAsync({
144
+ try: () => response.data.json() as Promise<User>,
145
+ catch: (cause) => UserServiceError.FetchFailed({ userId, cause }),
146
+ });
147
+ }
324
148
 
325
- startRecording: defineMutation({
326
- mutationKey: ['recorder', 'start'],
327
- mutationFn: async () => {
328
- const { error } = await services.recorder.startRecording();
329
- if (error) {
330
- return Err({
331
- title: "❌ Failed to start recording",
332
- description: error.message,
333
- action: { type: 'more-details', error }
334
- });
335
- }
336
-
337
- // Optimistically update cache
338
- queryClient.setQueryData(['recorder', 'state'], 'recording');
339
- return Ok(undefined);
340
- }
341
- })
342
- };
343
-
344
- // 3. Component Usage - Choose reactive or imperative based on needs
345
- // Reactive: Automatic state management (Svelte 5 requires accessor function)
346
- const recorderState = createQuery(() => recorder.getRecorderState.options);
149
+ // API handler: maps domain errors to HTTP responses
150
+ async function handleGetUser(request: Request, userId: string) {
151
+ const { data, error } = await getUser(userId);
347
152
 
348
- // Imperative: Direct execution for event handlers
349
- async function handleStartRecording() {
350
- const { error } = await recorder.startRecording.execute();
351
- // Or shorthand: await recorder.startRecording()
352
153
  if (error) {
353
- showToast(error.title, { description: error.description });
154
+ switch (error.name) {
155
+ case "NotFound":
156
+ return Response.json({ error: error.message }, { status: 404 });
157
+ case "FetchFailed":
158
+ return Response.json({ error: error.message }, { status: 502 });
159
+ }
354
160
  }
161
+
162
+ return Response.json(data);
355
163
  }
356
164
  ```
357
165
 
358
- ## Smart Return Type Narrowing
166
+ The full error chain is JSON-serializable at every level. Log it, send it over the wire, display it in a toast. The structure survives.
359
167
 
360
- The `catch` parameter in `trySync` and `tryAsync` enables smart return type narrowing based on your error handling strategy:
168
+ ## The Result type
361
169
 
362
- ### Recovery Pattern (Always Succeeds)
363
- ```typescript
364
- // ❌ Before: Mutable variable required
365
- let parsed: unknown;
366
- try {
367
- parsed = JSON.parse(riskyJson);
368
- } catch {
369
- parsed = [];
370
- }
371
- // Now use parsed...
170
+ The foundation is a simple discriminated union:
372
171
 
373
- // ✅ After: Clean, immutable pattern
374
- const { data: parsed } = trySync({
375
- try: () => JSON.parse(riskyJson),
376
- catch: () => Ok([])
377
- });
378
- // parsed is always defined and type-safe!
172
+ ```typescript
173
+ import { Ok, Err, trySync, tryAsync, type Result } from "wellcrafted/result";
379
174
 
380
- // When catch always returns Ok<T>, the function returns Ok<T>
381
- // This means no error checking needed - you can safely destructure and use data directly
175
+ type Ok<T> = { data: T; error: null };
176
+ type Err<E> = { error: E; data: null };
177
+ type Result<T, E> = Ok<T> | Err<E>;
382
178
  ```
383
179
 
384
- ### Propagation Pattern (May Fail)
385
- ```typescript
386
- const parseErrors = defineErrors({
387
- ParseError: () => ({
388
- message: "Invalid JSON",
389
- }),
390
- });
391
- const { ParseErr } = parseErrors;
180
+ Check `error` first, and TypeScript narrows `data` automatically:
392
181
 
393
- // When catch can return Err<E>, function returns Result<T, E>
394
- const mayFail = trySync({
395
- try: () => JSON.parse(riskyJson),
396
- catch: () => ParseErr()
397
- });
398
- // mayFail: Result<object, ParseError> - Must check for errors
399
- if (isOk(mayFail)) {
400
- console.log(mayFail.data); // Only safe after checking
182
+ ```typescript
183
+ const { data, error } = await someOperation();
184
+ if (error) {
185
+ // error is E, data is null
186
+ return;
401
187
  }
188
+ // data is T, error is null
402
189
  ```
403
190
 
404
- ### Mixed Strategy (Conditional Recovery)
405
- ```typescript
406
- const smartParse = trySync({
407
- try: () => JSON.parse(input),
408
- catch: () => {
409
- // Recover from empty input
410
- if (input.trim() === "") {
411
- return Ok({}); // Return Ok<T> for fallback
412
- }
413
- // Propagate other errors
414
- return ParseErr();
415
- }
416
- });
417
- // smartParse: Result<object, ParseError> - Mixed handling = Result type
418
- ```
191
+ ## Also in the box
419
192
 
420
- This eliminates unnecessary error checking when you always recover, while still requiring proper error handling when failures are possible.
193
+ ### Brand Types
421
194
 
422
- ## Why wellcrafted?
195
+ Create distinct types from primitives so TypeScript catches mix-ups at compile time. Zero runtime footprint — purely a type utility.
423
196
 
424
- JavaScript's `try-catch` has fundamental problems:
197
+ ```typescript
198
+ import type { Brand } from "wellcrafted/brand";
425
199
 
426
- 1. **Invisible Errors**: Function signatures don't show what errors can occur
427
- 2. **Lost in Transit**: `JSON.stringify(new Error())` loses critical information
428
- 3. **No Type Safety**: TypeScript can't help with `catch (error)` blocks
429
- 4. **Inconsistent**: Libraries throw different things (strings, errors, objects, undefined)
200
+ type UserId = string & Brand<"UserId">;
201
+ type OrderId = string & Brand<"OrderId">;
430
202
 
431
- wellcrafted solves these with simple, composable primitives that make errors:
432
- - **Explicit** in function signatures
433
- - **Serializable** across all boundaries
434
- - **Type-safe** with full TypeScript support
435
- - **Consistent** with structured error objects
203
+ function getUser(id: UserId) { /* ... */ }
436
204
 
437
- ## Service Pattern Best Practices
205
+ const userId = "abc" as UserId;
206
+ const orderId = "xyz" as OrderId;
207
+ getUser(userId); // compiles
208
+ getUser(orderId); // type error
209
+ ```
438
210
 
439
- Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:
211
+ ### Query Integration
440
212
 
441
- ### Factory Function Pattern
213
+ TanStack Query factories with a dual interface: `.options` for reactive components, callable for imperative use in event handlers.
442
214
 
443
215
  ```typescript
444
- import { defineErrors, type InferError } from "wellcrafted/error";
445
- import { Result, Ok } from "wellcrafted/result";
446
-
447
- // 1. Define service-specific errors with typed fields and message
448
- const recorderErrors = defineErrors({
449
- RecorderServiceError: (fields: { isRecording: boolean }) => ({
450
- ...fields,
451
- message: fields.isRecording ? "Already recording" : "Not currently recording",
452
- }),
216
+ import { createQueryFactories } from "wellcrafted/query";
217
+
218
+ const { defineQuery, defineMutation } = createQueryFactories(queryClient);
219
+
220
+ const userQuery = defineQuery({
221
+ queryKey: ["users", userId],
222
+ queryFn: () => getUser(userId), // returns Result<User, UserError>
453
223
  });
454
- const { RecorderServiceError, RecorderServiceErr } = recorderErrors;
455
- type RecorderServiceError = InferError<typeof recorderErrors, "RecorderServiceError">;
456
-
457
- // 2. Create service with factory function
458
- export function createRecorderService() {
459
- // Private state in closure
460
- let isRecording = false;
461
-
462
- // Return object with methods
463
- return {
464
- startRecording(): Result<void, RecorderServiceError> {
465
- if (isRecording) {
466
- return RecorderServiceErr({ isRecording });
467
- }
468
-
469
- isRecording = true;
470
- return Ok(undefined);
471
- },
472
-
473
- stopRecording(): Result<Blob, RecorderServiceError> {
474
- if (!isRecording) {
475
- return RecorderServiceErr({ isRecording });
476
- }
477
-
478
- isRecording = false;
479
- return Ok(new Blob(["audio data"]));
480
- }
481
- };
482
- }
483
224
 
484
- // 3. Export type
485
- export type RecorderService = ReturnType<typeof createRecorderService>;
225
+ // Reactive pass to useQuery (React) or createQuery (Svelte)
226
+ const query = createQuery(() => userQuery.options);
486
227
 
487
- // 4. Create singleton instance
488
- export const RecorderServiceLive = createRecorderService();
228
+ // Imperative direct execution for event handlers
229
+ const { data, error } = await userQuery.fetch();
489
230
  ```
490
231
 
491
- ### Platform-Specific Services
232
+ ## Comparison
492
233
 
493
- For services that need different implementations per platform:
234
+ | | wellcrafted | neverthrow | better-result | fp-ts | Effect |
235
+ |---|---|---|---|---|---|
236
+ | Error definition | `defineErrors` factories | Bring your own | `TaggedError` classes | Bring your own | Class-based with `_tag` |
237
+ | Error shape | Plain frozen objects | Any type | Class instances | Any type | Class instances |
238
+ | Composition | Manual `if (error)` | `.map().andThen()` | `Result.gen()` generators | Pipe operators | `yield*` generators |
239
+ | Bundle size | < 2KB | ~5KB | ~2KB | ~30KB | ~50KB |
240
+ | Syntax | async/await | Method chains | Method chains + generators | Pipe operators | Generators |
494
241
 
495
- ```typescript
496
- // types.ts - shared interface
497
- export type FileService = {
498
- readFile(path: string): Promise<Result<string, FileServiceError>>;
499
- writeFile(path: string, content: string): Promise<Result<void, FileServiceError>>;
500
- };
501
-
502
- // desktop.ts
503
- export function createFileServiceDesktop(): FileService {
504
- return {
505
- async readFile(path) {
506
- // Desktop implementation using Node.js APIs
507
- },
508
- async writeFile(path, content) {
509
- // Desktop implementation
510
- }
511
- };
512
- }
242
+ Every Result library gives you a container. wellcrafted gives you what goes inside it — then gets out of the way.
513
243
 
514
- // web.ts
515
- export function createFileServiceWeb(): FileService {
516
- return {
517
- async readFile(path) {
518
- // Web implementation using File API
519
- },
520
- async writeFile(path, content) {
521
- // Web implementation
522
- }
523
- };
524
- }
244
+ ## Philosophy
525
245
 
526
- // index.ts - runtime selection
527
- export const FileServiceLive = typeof window !== 'undefined'
528
- ? createFileServiceWeb()
529
- : createFileServiceDesktop();
530
- ```
246
+ wellcrafted is deliberately idiomatic to JavaScript. The `{ data, error }` shape isn't novel — it's the same pattern used by Supabase, SvelteKit load functions, and TanStack Query. We chose it because it's already familiar, already destructurable, and requires zero new mental models.
531
247
 
532
- ## Common Use Cases
248
+ The same principle applies throughout: async/await instead of generators, `switch` instead of `.match()`, plain objects instead of class hierarchies. The best abstractions are the ones your team already knows. wellcrafted adds type-safe error definition on top of patterns that JavaScript developers use every day — it doesn't ask you to learn a new programming paradigm to handle errors.
533
249
 
534
- <details>
535
- <summary><b>API Route Handler</b></summary>
250
+ ## API Reference
536
251
 
537
- ```typescript
538
- export async function GET(request: Request) {
539
- const result = await userService.getUser(params.id);
540
-
541
- if (result.error) {
542
- switch (result.error.name) {
543
- case "UserNotFoundError":
544
- return new Response("Not found", { status: 404 });
545
- case "DatabaseError":
546
- return new Response("Server error", { status: 500 });
547
- }
548
- }
549
-
550
- return Response.json(result.data);
551
- }
552
- ```
553
- </details>
252
+ ### Error functions
554
253
 
555
- <details>
556
- <summary><b>Form Validation</b></summary>
254
+ - **`defineErrors(config)`** — define multiple error factories in a single call. Each key becomes a variant; the value is a factory returning `{ message, ...fields }`. Every factory returns `Err<...>` directly.
255
+ - **`extractErrorMessage(error)`** — extract a readable string from any `unknown` error value.
557
256
 
558
- ```typescript
559
- import { defineErrors, type InferError } from "wellcrafted/error";
257
+ ### Error types
560
258
 
561
- const formErrors = defineErrors({
562
- FormError: (fields: { fields: Record<string, string[]> }) => ({
563
- ...fields,
564
- message: `Validation failed for: ${Object.keys(fields.fields).join(", ")}`,
565
- }),
566
- });
567
- const { FormErr } = formErrors;
568
- type FormError = InferError<typeof formErrors, "FormError">;
259
+ - **`InferErrors<T>`** extract union of all error types from a `defineErrors` return value.
260
+ - **`InferError<T>`** extract a single variant's error type from one factory.
569
261
 
570
- function validateLoginForm(data: unknown): Result<LoginData, FormError> {
571
- const errors: Record<string, string[]> = {};
262
+ ### Result functions
572
263
 
573
- if (!isValidEmail(data?.email)) {
574
- errors.email = ["Invalid email format"];
575
- }
264
+ - **`Ok(data)`** — create a success result
265
+ - **`Err(error)`** create a failure result
266
+ - **`trySync({ try, catch })`** — wrap a synchronous throwing operation
267
+ - **`tryAsync({ try, catch })`** — wrap an async throwing operation
268
+ - **`isOk(result)` / `isErr(result)`** — type guards
269
+ - **`unwrap(result)`** — extract data or throw error
270
+ - **`resolve(value)`** — handle values that may or may not be Results
271
+ - **`partitionResults(results)`** — split an array of Results into separate ok/err arrays
576
272
 
577
- if (Object.keys(errors).length > 0) {
578
- return FormErr({ fields: errors });
579
- }
273
+ ### Query functions
580
274
 
581
- return Ok(data as LoginData);
582
- }
583
- ```
584
- </details>
275
+ - **`createQueryFactories(client)`** create query/mutation factories for TanStack Query
276
+ - **`defineQuery(options)`** — define a query with dual interface (`.options` for reactive, callable for imperative)
277
+ - **`defineMutation(options)`** — define a mutation with dual interface
585
278
 
586
- <details>
587
- <summary><b>React Hook</b></summary>
279
+ ### Standard Schema
588
280
 
589
- ```typescript
590
- function useUser(id: number) {
591
- const [state, setState] = useState<{
592
- loading: boolean;
593
- user?: User;
594
- error?: ApiError;
595
- }>({ loading: true });
596
-
597
- useEffect(() => {
598
- fetchUser(id).then(result => {
599
- if (result.error) {
600
- setState({ loading: false, error: result.error });
601
- } else {
602
- setState({ loading: false, user: result.data });
603
- }
604
- });
605
- }, [id]);
606
-
607
- return state;
608
- }
609
- ```
610
- </details>
281
+ - **`ResultSchema(dataSchema, errorSchema)`** — [Standard Schema](https://github.com/standard-schema/standard-schema) wrapper for Result types, interoperable with any validator that supports the spec.
611
282
 
612
- ## Comparison with Alternatives
283
+ ### Other types
613
284
 
614
- | | wellcrafted | fp-ts | Effect | neverthrow |
615
- |---|---|---|---|---|
616
- | **Bundle Size** | < 2KB | ~30KB | ~50KB | ~5KB |
617
- | **Learning Curve** | Minimal | Steep | Steep | Moderate |
618
- | **Syntax** | Native async/await | Pipe operators | Generators | Method chains |
619
- | **Type Safety** | ✅ Full | ✅ Full | ✅ Full | ✅ Full |
620
- | **Serializable Errors** | ✅ Built-in | ❌ Classes | ❌ Classes | ❌ Classes |
621
- | **Runtime Overhead** | Zero | Minimal | Moderate | Minimal |
285
+ - **`Result<T, E>`** union of `Ok<T> | Err<E>`
286
+ - **`Brand<T, B>`** — branded type wrapper for distinct primitives
622
287
 
623
- ## Advanced Usage
288
+ ## AI Agent Skills
624
289
 
625
- For comprehensive examples, service layer patterns, framework integrations, and migration guides, see the **[full documentation →](https://docs.wellcrafted.dev)**
290
+ If you use an AI coding agent (Claude Code, Cursor, etc.), teach it how to use wellcrafted correctly:
626
291
 
627
- ## API Reference
292
+ ```bash
293
+ npx skills add wellcrafted-dev/wellcrafted
294
+ ```
628
295
 
629
- ### Result Functions
630
- - **`Ok(data)`** - Create success result
631
- - **`Err(error)`** - Create failure result
632
- - **`isOk(result)`** - Type guard for success
633
- - **`isErr(result)`** - Type guard for failure
634
- - **`trySync(options)`** - Wrap throwing function
635
- - **`tryAsync(options)`** - Wrap async function
636
- - **`unwrap(result)`** - Extract data or throw error
637
- - **`resolve(value)`** - Handle values that may or may not be Results
638
- - **`partitionResults(results)`** - Split array into oks/errs
639
-
640
- ### Query Functions
641
- - **`createQueryFactories(client)`** - Create query/mutation factories for TanStack Query
642
- - **`defineQuery(options)`** - Define a query with dual interface (`.options` + callable/`.ensure()`/`.fetch()`)
643
- - **`defineMutation(options)`** - Define a mutation with dual interface (`.options` + callable/`.execute()`)
644
-
645
- ### Error Functions
646
- - **`defineErrors(definitions)`** - Define multiple error factories in a single declaration
647
- - Each key becomes an error name; the value is a factory function returning fields + `message`
648
- - Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped) for each key
649
- - Factory functions receive typed fields and return `{ ...fields, message }`
650
- - No-field errors use `() => ({ message: '...' })`
651
- - `name` is a reserved key — prevented at compile time
652
- - **`extractErrorMessage(error)`** - Extract readable message from unknown error
653
-
654
- ### Types
655
- - **`Result<T, E>`** - Union of Ok<T> | Err<E>
656
- - **`Ok<T>`** - Success result type
657
- - **`Err<E>`** - Error result type
658
- - **`InferError<TErrors, TName>`** - Extract error type from `defineErrors` result
659
- - **`Brand<T, B>`** - Branded type wrapper
660
- - **`ExtractOkFromResult<R>`** - Extract Ok variant from Result union
661
- - **`ExtractErrFromResult<R>`** - Extract Err variant from Result union
662
- - **`UnwrapOk<R>`** - Extract success value type from Result
663
- - **`UnwrapErr<R>`** - Extract error value type from Result
296
+ This installs 5 skills that teach your agent the patterns, anti-patterns, and API conventions:
664
297
 
665
- ## Development Setup
298
+ | Skill | What it teaches |
299
+ | --- | --- |
300
+ | `define-errors` | `defineErrors` variants, `extractErrorMessage`, `InferErrors`/`InferError` type extraction |
301
+ | `result-types` | `Ok`, `Err`, `trySync`/`tryAsync`, the `{ data, error }` destructuring pattern |
302
+ | `query-factories` | `createQueryFactories`, `defineQuery`/`defineMutation`, dual interface (reactive + imperative) |
303
+ | `branded-types` | `Brand<T>`, brand constructor pattern, when to add runtime validation |
304
+ | `patterns` | Architectural style guide: control flow, factory composition, service layers, error composition |
666
305
 
667
- ### Peer Directory Requirement
306
+ Skills work with any agent that supports [`npx skills`](https://www.npmjs.com/package/skills). Install once, update with `npx skills update`.
668
307
 
669
- Wellcrafted shares AI agent skills (`.agents/skills/`, `.claude/skills/`) with the [Epicenter](https://github.com/EpicenterHQ/epicenter) repo via relative symlinks. Epicenter is the source of truth for skill definitions — they're authored and maintained there, and wellcrafted consumes them to stay in sync.
670
308
 
671
- **Both repos must be sibling directories under the same parent:**
309
+ ## Development Setup
672
310
 
673
- ```
674
- Code/
675
- ├── epicenter/ # Source of truth for skills
676
- │ └── .agents/skills/
677
- └── wellcrafted/ # Symlinks to epicenter
678
- ├── .agents/skills/<name> → ../../../epicenter/.agents/skills/<name>
679
- └── .claude/skills/<name> → ../../.agents/skills/<name>
680
- ```
311
+ ### AI Agent Skills
681
312
 
682
- If symlinks appear broken after cloning, ensure epicenter is cloned alongside wellcrafted:
313
+ AI agent skills are managed via [`npx skills`](https://www.npmjs.com/package/skills), sourced from [Epicenter](https://github.com/EpicenterHQ/epicenter). Only skills relevant to wellcrafted's domain are installed.
683
314
 
684
315
  ```bash
685
- cd "$(git rev-parse --show-toplevel)/.."
686
- git clone https://github.com/EpicenterHQ/epicenter.git
316
+ # Install skills (already committed, but can be refreshed)
317
+ npx skills add EpicenterHQ/epicenter --skill error-handling --skill define-errors -a claude-code -y
318
+
319
+ # Update all installed skills
320
+ npx skills update
321
+
322
+ # List installed skills
323
+ npx skills list
687
324
  ```
688
325
 
689
326
  ## License
690
327
 
691
328
  MIT
692
-
693
- ---
694
-
695
- Made with ❤️ by developers who believe error handling should be delightful.