npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

wellcrafted

v0.29.0

Published

Delightful TypeScript patterns for elegant, type-safe applications

Downloads

2,411

Readme

wellcrafted

npm version TypeScript License: MIT Bundle Size

Delightful TypeScript utilities for elegant, type-safe applications

Transform unpredictable errors into type-safe results

// ❌ Before: Which errors can this throw? 🤷
try {
  await saveUser(user);
} catch (error) {
  // ... good luck debugging in production
}

// ✅ After: Every error is visible and typed
const { data, error } = await saveUser(user);
if (error) {
  switch (error.name) {
    case "ValidationError": 
      showToast(`Invalid ${error.context.field}`);
      break;
    case "AuthError":
      redirectToLogin();
      break;
    // TypeScript ensures you handle all cases!
  }
}

A collection of simple, powerful primitives

🎯 Result Type

Make errors explicit in function signatures

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return Err("Division by zero");
  return Ok(a / b);
}

🏷️ Brand Types

Create distinct types from primitives

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

// TypeScript prevents mixing them up!
function getUser(id: UserId) { /* ... */ }

📋 Tagged Errors

Structured, serializable errors with a fluent API

import { createTaggedError } from "wellcrafted/error";

// Minimal by default - only name and message
const { ValidationError } = createTaggedError("ValidationError");
ValidationError({ message: "Email is required" });

// Chain to add context and cause when needed
const { ApiError } = createTaggedError("ApiError")
  .withContext<{ endpoint: string }>()
  .withCause<NetworkError | undefined>();

🔄 Query Integration

Seamless TanStack Query integration with dual interfaces

import { createQueryFactories } from "wellcrafted/query";
import { QueryClient } from "@tanstack/query-core";

const queryClient = new QueryClient();
const { defineQuery, defineMutation } = createQueryFactories(queryClient);

// Define operations that return Result types
const userQuery = defineQuery({
  queryKey: ['users', userId],
  queryFn: () => getUserFromAPI(userId) // Returns Result<User, ApiError>
});

// Use reactively in components with automatic state management
const query = createQuery(userQuery.options());
// query.data, query.error, query.isPending all managed automatically

// Or use imperatively for direct execution (perfect for event handlers)
const { data, error } = await userQuery.fetch();
if (error) {
  showErrorToast(error.message);
  return;
}
// Use data...

Installation

npm install wellcrafted

Quick Start

import { tryAsync } from "wellcrafted/result";
import { createTaggedError, type AnyTaggedError } from "wellcrafted/error";

// Define your error with factory function
const { ApiError, ApiErr } = createTaggedError("ApiError")
  .withContext<{ endpoint: string }>()
  .withCause<AnyTaggedError | undefined>();
type ApiError = ReturnType<typeof ApiError>;

// Wrap any throwing operation
const { data, error } = await tryAsync({
  try: () => fetch('/api/user').then(r => r.json()),
  catch: (e) => ApiErr({
    message: "Failed to fetch user",
    context: { endpoint: '/api/user' },
    cause: { name: "FetchError", message: String(e) }
  })
});

if (error) {
  console.error(`${error.name}: ${error.message}`);
} else {
  console.log("User:", data);
}

Core Features

🎯 Explicit Error Handling
All errors visible in function signatures

📦 Serialization-Safe
Plain objects work everywhere

✨ Elegant API
Clean, intuitive patterns

🔍 Zero Magic
~50 lines of core code

🚀 Lightweight
Zero dependencies, < 2KB

🎨 Composable
Mix and match utilities

The Result Pattern Explained

The Result type makes error handling explicit and type-safe:

type Ok<T> = { data: T; error: null };
type Err<E> = { error: E; data: null };
type Result<T, E> = Ok<T> | Err<E>;

This creates a discriminated union where TypeScript automatically narrows types:

if (result.error) {
  // TypeScript knows: error is E, data is null
} else {
  // TypeScript knows: data is T, error is null
}

Basic Patterns

Handle Results with Destructuring

const { data, error } = await someOperation();

if (error) {
  // Handle error with full type safety
  return;
}

// Use data - TypeScript knows it's safe

Wrap Unsafe Operations

// Define errors with context and cause
const { ParseError, ParseErr } = createTaggedError("ParseError")
  .withContext<{ input: string }>();

const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
  .withContext<{ url: string }>();

// Synchronous
const result = trySync({
  try: () => JSON.parse(jsonString),
  catch: () => ParseErr({
    message: "Invalid JSON",
    context: { input: jsonString }
  })
});

// Asynchronous
const result = await tryAsync({
  try: () => fetch(url),
  catch: () => NetworkErr({
    message: "Request failed",
    context: { url }
  })
});

Real-World Service + Query Layer Example

// 1. Service Layer - Pure business logic
import { createTaggedError } from "wellcrafted/error";
import { tryAsync, Result, Ok } from "wellcrafted/result";

const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
  .withContext<{ currentState?: string; permissions?: string }>();
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;

export function createRecorderService() {
  let isRecording = false;
  let currentBlob: Blob | null = null;

  return {
    async startRecording(): Promise<Result<void, RecorderServiceError>> {
      if (isRecording) {
        return RecorderServiceErr({
          message: "Already recording",
          context: { currentState: 'recording' }
        });
      }

      return tryAsync({
        try: async () => {
          const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
          const recorder = new MediaRecorder(stream);
          // ... recording setup
          isRecording = true;
        },
        catch: () => RecorderServiceErr({
          message: "Failed to start recording",
          context: { permissions: 'microphone' }
        })
      });
    },

    async stopRecording(): Promise<Result<Blob, RecorderServiceError>> {
      if (!isRecording) {
        return RecorderServiceErr({
          message: "Not currently recording",
          context: { currentState: 'idle' }
        });
      }

      // Stop recording and return blob...
      isRecording = false;
      return Ok(currentBlob!);
    }
  };
}

// 2. Query Layer - Adds caching, reactivity, and UI error handling
import { createQueryFactories } from "wellcrafted/query";

const { defineQuery, defineMutation } = createQueryFactories(queryClient);

export const recorder = {
  getRecorderState: defineQuery({
    queryKey: ['recorder', 'state'],
    queryFn: async () => {
      const { data, error } = await services.recorder.getState();
      if (error) {
        // Transform service error to UI-friendly error
        return Err({
          title: "❌ Failed to get recorder state",
          description: error.message,
          action: { type: 'retry' }
        });
      }
      return Ok(data);
    },
    refetchInterval: 1000, // Poll for state changes
  }),

  startRecording: defineMutation({
    mutationKey: ['recorder', 'start'],
    mutationFn: async () => {
      const { error } = await services.recorder.startRecording();
      if (error) {
        return Err({
          title: "❌ Failed to start recording",  
          description: error.message,
          action: { type: 'more-details', error }
        });
      }

      // Optimistically update cache
      queryClient.setQueryData(['recorder', 'state'], 'recording');
      return Ok(undefined);
    }
  })
};

// 3. Component Usage - Choose reactive or imperative based on needs
// Reactive: Automatic state management
const recorderState = createQuery(recorder.getRecorderState.options());

// Imperative: Direct execution for event handlers
async function handleStartRecording() {
  const { error } = await recorder.startRecording.execute();
  if (error) {
    showToast(error.title, { description: error.description });
  }
}

Smart Return Type Narrowing

The catch parameter in trySync and tryAsync enables smart return type narrowing based on your error handling strategy:

Recovery Pattern (Always Succeeds)

// ❌ Before: Mutable variable required
let parsed: unknown;
try {
  parsed = JSON.parse(riskyJson);
} catch {
  parsed = [];
}
// Now use parsed...

// ✅ After: Clean, immutable pattern
const { data: parsed } = trySync({
  try: () => JSON.parse(riskyJson),
  catch: () => Ok([])
});
// parsed is always defined and type-safe!

// When catch always returns Ok<T>, the function returns Ok<T>
// This means no error checking needed - you can safely destructure and use data directly

Propagation Pattern (May Fail)

const { ParseError, ParseErr } = createTaggedError("ParseError");

// When catch can return Err<E>, function returns Result<T, E>
const mayFail = trySync({
  try: () => JSON.parse(riskyJson),
  catch: () => ParseErr({ message: "Invalid JSON" })
});
// mayFail: Result<object, ParseError> - Must check for errors
if (isOk(mayFail)) {
  console.log(mayFail.data); // Only safe after checking
}

Mixed Strategy (Conditional Recovery)

const smartParse = trySync({
  try: () => JSON.parse(input),
  catch: () => {
    // Recover from empty input
    if (input.trim() === "") {
      return Ok({}); // Return Ok<T> for fallback
    }
    // Propagate other errors
    return ParseErr({ message: "Parse failed" });
  }
});
// smartParse: Result<object, ParseError> - Mixed handling = Result type

This eliminates unnecessary error checking when you always recover, while still requiring proper error handling when failures are possible.

Why wellcrafted?

JavaScript's try-catch has fundamental problems:

  1. Invisible Errors: Function signatures don't show what errors can occur
  2. Lost in Transit: JSON.stringify(new Error()) loses critical information
  3. No Type Safety: TypeScript can't help with catch (error) blocks
  4. Inconsistent: Libraries throw different things (strings, errors, objects, undefined)

wellcrafted solves these with simple, composable primitives that make errors:

  • Explicit in function signatures
  • Serializable across all boundaries
  • Type-safe with full TypeScript support
  • Consistent with structured error objects

Service Pattern Best Practices

Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:

Factory Function Pattern

import { createTaggedError } from "wellcrafted/error";
import { Result, Ok } from "wellcrafted/result";

// 1. Define service-specific errors with typed context
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
  .withContext<{ isRecording: boolean }>();
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;

// 2. Create service with factory function
export function createRecorderService() {
  // Private state in closure
  let isRecording = false;

  // Return object with methods
  return {
    startRecording(): Result<void, RecorderServiceError> {
      if (isRecording) {
        return RecorderServiceErr({
          message: "Already recording",
          context: { isRecording }
        });
      }

      isRecording = true;
      return Ok(undefined);
    },

    stopRecording(): Result<Blob, RecorderServiceError> {
      if (!isRecording) {
        return RecorderServiceErr({
          message: "Not currently recording",
          context: { isRecording }
        });
      }

      isRecording = false;
      return Ok(new Blob(["audio data"]));
    }
  };
}

// 3. Export type
export type RecorderService = ReturnType<typeof createRecorderService>;

// 4. Create singleton instance
export const RecorderServiceLive = createRecorderService();

Platform-Specific Services

For services that need different implementations per platform:

// types.ts - shared interface
export type FileService = {
  readFile(path: string): Promise<Result<string, FileServiceError>>;
  writeFile(path: string, content: string): Promise<Result<void, FileServiceError>>;
};

// desktop.ts
export function createFileServiceDesktop(): FileService {
  return {
    async readFile(path) {
      // Desktop implementation using Node.js APIs
    },
    async writeFile(path, content) {
      // Desktop implementation
    }
  };
}

// web.ts  
export function createFileServiceWeb(): FileService {
  return {
    async readFile(path) {
      // Web implementation using File API
    },
    async writeFile(path, content) {
      // Web implementation
    }
  };
}

// index.ts - runtime selection
export const FileServiceLive = typeof window !== 'undefined' 
  ? createFileServiceWeb()
  : createFileServiceDesktop();

Common Use Cases

export async function GET(request: Request) {
  const result = await userService.getUser(params.id);
  
  if (result.error) {
    switch (result.error.name) {
      case "UserNotFoundError":
        return new Response("Not found", { status: 404 });
      case "DatabaseError":
        return new Response("Server error", { status: 500 });
    }
  }
  
  return Response.json(result.data);
}
const { FormError, FormErr } = createTaggedError("FormError")
  .withContext<{ fields: Record<string, string[]> }>();

function validateLoginForm(data: unknown): Result<LoginData, FormError> {
  const errors: Record<string, string[]> = {};

  if (!isValidEmail(data?.email)) {
    errors.email = ["Invalid email format"];
  }

  if (Object.keys(errors).length > 0) {
    return FormErr({
      message: "Validation failed",
      context: { fields: errors }
    });
  }

  return Ok(data as LoginData);
}
function useUser(id: number) {
  const [state, setState] = useState<{
    loading: boolean;
    user?: User;
    error?: ApiError;
  }>({ loading: true });

  useEffect(() => {
    fetchUser(id).then(result => {
      if (result.error) {
        setState({ loading: false, error: result.error });
      } else {
        setState({ loading: false, user: result.data });
      }
    });
  }, [id]);

  return state;
}

Comparison with Alternatives

| | wellcrafted | fp-ts | Effect | neverthrow | |---|---|---|---|---| | Bundle Size | < 2KB | ~30KB | ~50KB | ~5KB | | Learning Curve | Minimal | Steep | Steep | Moderate | | Syntax | Native async/await | Pipe operators | Generators | Method chains | | Type Safety | ✅ Full | ✅ Full | ✅ Full | ✅ Full | | Serializable Errors | ✅ Built-in | ❌ Classes | ❌ Classes | ❌ Classes | | Runtime Overhead | Zero | Minimal | Moderate | Minimal |

Advanced Usage

For comprehensive examples, service layer patterns, framework integrations, and migration guides, see the full documentation →

API Reference

Result Functions

  • Ok(data) - Create success result
  • Err(error) - Create failure result
  • isOk(result) - Type guard for success
  • isErr(result) - Type guard for failure
  • trySync(options) - Wrap throwing function
  • tryAsync(options) - Wrap async function
  • unwrap(result) - Extract data or throw error
  • resolve(value) - Handle values that may or may not be Results
  • partitionResults(results) - Split array into oks/errs

Query Functions

  • createQueryFactories(client) - Create query/mutation factories for TanStack Query
  • defineQuery(options) - Define a query with dual interface (.options() + .fetch())
  • defineMutation(options) - Define a mutation with dual interface (.options() + .execute())

Error Functions

  • createTaggedError(name) - Creates error factory functions with fluent API
    • Returns {ErrorName} (plain error) and {ErrorName}Err (Err-wrapped)
    • Default: minimal errors with only name and message
    • Chain .withContext<T>() to add typed context
    • Chain .withCause<T>() to add typed cause
    • Include | undefined in type to make property optional but typed
  • extractErrorMessage(error) - Extract readable message from unknown error

Types

  • Result<T, E> - Union of Ok | Err
  • Ok<T> - Success result type
  • Err<E> - Error result type
  • TaggedError<T> - Structured error type
  • Brand<T, B> - Branded type wrapper
  • ExtractOkFromResult<R> - Extract Ok variant from Result union
  • ExtractErrFromResult<R> - Extract Err variant from Result union
  • UnwrapOk<R> - Extract success value type from Result
  • UnwrapErr<R> - Extract error value type from Result

License

MIT


Made with ❤️ by developers who believe error handling should be delightful.