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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@andersoncustodio/railway

v1.0.0

Published

Railway Oriented Programming for TypeScript

Readme

Railway

A lightweight Railway Oriented Programming toolkit for TypeScript. Models operations as two parallel tracks, success and failure, so your domain code stays free of try/catch and error handling stays type-safe and explicit.

Install

npm install @andersoncustodio/railway

Primitives

| | Purpose | |---|---| | Result<T> | Discriminated union (Ok<T> \| Err) for domain logic. | | Outcome<T> | API-facing response with HTTP status, data, meta, errors. | | ErrorDetail | A single field-level error (code, field, message, meta). | | ErrorCollector | Accumulates errors from multiple validations. | | HaltError | Structured exception that carries the error track across the call stack. | | HttpStatus | Standard HTTP status codes enum. |

Value Object with Result

A classic DDD pattern: the value object has a private constructor and a static factory returning Result, so invalid state is impossible to construct.

import { Result, ErrorDetail } from '@andersoncustodio/railway';

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export class Email {
  private constructor(readonly value: string) {}

  static create(input: string): Result<Email> {
    const value = input.trim();

    if (value.length === 0) {
      return Result.err([
        ErrorDetail.from({ code: 'required', field: 'email', message: 'Email is required' }),
      ]);
    }

    if (!EMAIL_REGEX.test(value)) {
      return Result.err([
        ErrorDetail.from({ code: 'invalid_format', field: 'email', message: 'Invalid email' }),
      ]);
    }

    return Result.ok(new Email(value.toLowerCase()));
  }
}

const r = Email.create('foo@bar');
if (r.isErr()) {
  console.log(r.firstError.message); // "Invalid email"
} else {
  r.value; // Email - type is narrowed, no casting
}

No hidden control flow: the signature tells you it can fail, and the compiler forces you to handle it.

Aggregating errors with ErrorCollector

Validate many value objects and report all errors at once instead of failing on the first.

import { Result, ErrorCollector } from '@andersoncustodio/railway';

class User {
  private constructor(readonly name: Name, readonly email: Email) {}

  static create(params: { name: string; email: string }): Result<User> {
    const errorCollector = ErrorCollector.create();

    const name = Name.create(params.name).unwrap(errorCollector);
    const email = Email.create(params.email).unwrap(errorCollector);

    if (errorCollector.hasErrors()) return Result.err(errorCollector.errors());

    return Result.ok(new User(name, email));
  }
}

unwrap(errorCollector) is the key: on Err, errors flow into the collector and the call returns never. Once hasErrors() passes, name and email are narrowed to their valid types.

Remapping field names

unwrap accepts a second argument, a fieldMapper, that rewrites the field of each error as it flows into the collector. Useful when validating nested structures or lists, where the inner value object doesn't know its position in the parent.

class Order {
  private constructor(readonly items: OrderItem[]) {}

  static create(params: { items: ItemInput[] }): Result<Order> {
    const errorCollector = ErrorCollector.create();

    const items = params.items.map((item, i) =>
      OrderItem.create(item).unwrap(errorCollector, (field) => `items[${i}].${field}`)
    );

    if (errorCollector.hasErrors()) return Result.err(errorCollector.errors());

    return Result.ok(new Order(items));
  }
}

An error emitted by OrderItem.create with field: 'sku' becomes field: 'items[2].sku' in the final payload, giving the client a path it can use to highlight the offending input.

From domain to HTTP with Outcome

Outcome wraps a domain operation for HTTP delivery, carrying the right status code, a machine-readable code, and a structured payload.

import { Outcome, HttpStatus, ErrorCollector } from '@andersoncustodio/railway';

async function createUserHandler(cmd: CreateUserCommand): Promise<Outcome<{ data: { id: string } }>> {
  const errorCollector = ErrorCollector.create();
  const user = User.create({ name: cmd.name, email: cmd.email }).unwrap(errorCollector);

  if (errorCollector.hasErrors()) {
    return Outcome.err({
      code: 'user.validation_error',
      message: 'Validation failed',
      errors: errorCollector.errors(),
    });
  }

  const existing = await repository.findByEmail(user.email);
  if (existing) {
    return Outcome.err({
      status: HttpStatus.CONFLICT,
      code: 'user.email_taken',
      message: 'Email already in use',
    });
  }

  await repository.save(user);
  return Outcome.ok({
    status: HttpStatus.CREATED,
    data: { id: user.id },
  });
}

unwrap(errorCollector) returns User on success; on failure, errors flow into the collector and the happy path exits through the hasErrors() guard. Each failure branch maps cleanly to a distinct HTTP status: 422 (default), 409, etc. Success returns 201 Created with the payload.

License

MIT