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

@roastery/pantry

v0.1.0

Published

Shared utilities, use cases, DTOs, and domain abstractions for the Roastery CMS ecosystem.

Readme

@roastery/pantry

Shared utilities, use cases, DTOs, and domain abstractions for the Roastery CMS ecosystem — reusable building blocks for entity CRUD, pagination, slug validation, and repository contracts.

Checked with Biome

Overview

pantry provides cross-cutting application and domain concerns shared across Roastery services:

  • Repository contracts — Granular ICan* capability interfaces plus the IEntityReader / IEntityWriter / IEntityRepository aggregates, so a repository declares exactly the surface it exposes.
  • Use cases — Generic, entity-agnostic CRUD orchestration: find by id-or-slug, list a page, create (plain and slug-guarded), update, and delete.
  • Factoriesmake* helpers that wire a use case (or the slug checker) to its dependencies and hand back a ready instance.
  • SlugUniquenessCheckerService — Domain service that verifies slug availability.
  • DTOs & utilities — Schema-validated Data Transfer Objects for common query parameters, plus response-map and pagination helpers.
  • Constants — Shared configuration values for pagination limits and cache expiration.

Technologies

| Tool | Purpose | |------|---------| | @roastery/terroir | Schema validation, exception hierarchy, and TypeBox re-exports | | @roastery/beans | Entity abstractions, UUID utilities, and collection schemas | | @roastery/barista | HTTP status codes and exception-to-status mapping | | tsup | Bundling to ESM + CJS with .d.ts generation | | Bun | Runtime, test runner, and package manager | | Knip | Unused exports and dependency detection | | Husky + commitlint | Git hooks and conventional commit enforcement |

Installation

Install the package and its peer dependencies:

bun add @roastery/pantry @roastery/terroir @roastery/beans @roastery/barista typescript

Every export ships with full TSDoc, so signatures, parameters, and examples surface directly in your editor on hover.

Local development (link)

If you're developing pantry alongside another project, you can link it locally:

# Inside the pantry directory
bun run setup  # builds and registers the link

# Inside your consuming project
bun link @roastery/pantry

Repository contracts

Granular capability interfaces let a repository declare exactly the operations it supports — implement only what you need, and use cases depend on the narrowest contract that does the job. Every contract is generic over a single EntityType (which must extend IEntity<t.TSchema>), except ICanFindBy, which derives a second SchemaType generic from the entity, and ICanCount, which takes none.

import type {
  ICanCreate,
  ICanFind,
  ICanFindBy,
  ICanFindMany,
  ICanFindManyByIds,
  ICanUpdate,
  ICanDelete,
  ICanCount,
} from "@roastery/pantry/domain/types/repositories";

| Contract | Method | Description | |----------|--------|-------------| | ICanCreate<E> | create(data: E): Promise<void> | Persists a newly built entity. | | ICanFind<E> | find(id: string): Promise<E \| null> | Fetches a single entity by its UUID. | | ICanFindBy<E, S?> | findBy(property, value): Promise<E \| null> | Fetches a single entity by any schema property. S (the entity's schema) is derived automatically. | | ICanFindMany<E> | findMany(page: number): Promise<E[]> | Fetches one 1-based page; page size is fixed at MAX_ITEMS_PER_QUERY. | | ICanFindManyByIds<E> | findManyByIds(ids: string[]): Promise<Array<E \| null>> | Batch lookup preserving order: same length/positions as ids, with null for every id that has no match. | | ICanUpdate<E> | update(data: E): Promise<void> | Persists changes to an existing entity. | | ICanDelete<E> | delete(data: E): Promise<void> | Removes an entity. | | ICanCount | count(): Promise<number> | Reports the total number of stored entities. |

Aggregates

When a repository needs a broad surface, compose the aggregates instead of listing every ICan* by hand:

import type {
  IEntityReader,
  IEntityWriter,
  IEntityRepository,
} from "@roastery/pantry/domain/types/repositories";

| Aggregate | Combines | |-----------|----------| | IEntityReader<E> | ICanFind + ICanFindBy + ICanFindMany + ICanFindManyByIds + ICanCount | | IEntityWriter<E> | ICanCreate + ICanUpdate + ICanDelete | | IEntityRepository<E> | IEntityReader + IEntityWriter (full CRUD) |

Capsules typically extend IEntityRepository with their entity-specific reads (e.g. findManyByType), keeping the common surface inherited from here.


Use cases

Generic, entity-agnostic orchestration. Each use case depends on the narrowest repository contract it needs and exposes a single run method.

FindEntityByTypeUseCase

Resolves an entity from a repository by either its UUID or its slug, auto-detecting the input via detectEntry and delegating to find or findBy. Throws ResourceNotFoundException when nothing matches.

import { FindEntityByTypeUseCase } from "@roastery/pantry/application/use-cases";
import type { ICanFind, ICanFindBy } from "@roastery/pantry/domain/types/repositories";

class PostRepository implements ICanFind<Post>, ICanFindBy<Post> {
  async find(id: string): Promise<Post | null> { /* ... */ }
  async findBy(property, value): Promise<Post | null> { /* ... */ }
}

const useCase = new FindEntityByTypeUseCase(new PostRepository());

// Resolves by UUID
await useCase.run("550e8400-e29b-41d4-a716-446655440000", "posts");

// Resolves by slug
await useCase.run("my-first-post", "posts");

// Throws ResourceNotFoundException if not found

FindManyEntitiesUseCase

Lists a single page of entities alongside the pagination counters. totalPages is derived from the repository's raw count via getNumberOfPages.

import { FindManyEntitiesUseCase } from "@roastery/pantry/application/use-cases";

const useCase = new FindManyEntitiesUseCase(repository); // ICanFindMany & ICanCount

const { value, count, totalPages } = await useCase.run(1);
// value: Post[]  ·  count: number  ·  totalPages: number

CreateEntityUseCase

Builds an entity from its create input via an EntityFactory and persists it.

import { CreateEntityUseCase } from "@roastery/pantry/application/use-cases";

const useCase = new CreateEntityUseCase(repository, entityFactory); // ICanCreate + EntityFactory

const post = await useCase.run({ title: "Hello", slug: "hello" });

CreateSluggedEntityUseCase

Same as CreateEntityUseCase, but for slug-bearing entities: it rejects the input with ResourceAlreadyExistsException when the built entity's slug is already taken.

import { CreateSluggedEntityUseCase } from "@roastery/pantry/application/use-cases";

const useCase = new CreateSluggedEntityUseCase(
  repository,             // ICanCreate
  slugUniquenessChecker,  // SlugUniquenessCheckerService
  entityFactory,          // EntityFactory
);

const post = await useCase.run({ title: "Hello", slug: "hello" });
// Throws ResourceAlreadyExistsException if the slug already exists

UpdateEntityUseCase

Resolves an entity by UUID or slug, applies a partial set of field updates through EntityUpdater, and persists the rebuilt entity. Throws ResourceNotFoundException when no record matches.

import { FindEntityByTypeUseCase, UpdateEntityUseCase } from "@roastery/pantry/application/use-cases";

const findEntityByType = new FindEntityByTypeUseCase(repository); // ICanFind & ICanFindBy
const useCase = new UpdateEntityUseCase(repository, findEntityByType); // ICanUpdate

const updated = await useCase.run("550e8400-e29b-41d4-a716-446655440000", { title: "New title" }, "posts");

DeleteEntityUseCase

Resolves the entity by UUID or slug via FindEntityByTypeUseCase, then deletes it. Throws ResourceNotFoundException when no record matches.

import { DeleteEntityUseCase, FindEntityByTypeUseCase } from "@roastery/pantry/application/use-cases";

const findEntityByType = new FindEntityByTypeUseCase(repository); // ICanFind & ICanFindBy
const useCase = new DeleteEntityUseCase(repository, findEntityByType); // ICanDelete

await useCase.run("550e8400-e29b-41d4-a716-446655440000", "posts");

Factories

Each use case (and the slug checker) has a matching make* factory that wires its dependencies and returns a ready-to-use instance — handy when composing capsules at the infrastructure layer.

import {
  makeCreateEntityUseCase,
  makeCreateSluggedEntityUseCase,
  makeFindEntityByTypeUseCase,
  makeFindManyEntitiesUseCase,
  makeUpdateEntityUseCase,
  makeDeleteEntityUseCase,
} from "@roastery/pantry/infra/factories/application/use-cases";
import { makeSlugUniquenessCheckerService } from "@roastery/pantry/infra/factories/domain/services";

const create = makeCreateEntityUseCase(repository, entityFactory);
const list = makeFindManyEntitiesUseCase(repository);
const checker = makeSlugUniquenessCheckerService(reader);

Every factory mirrors the constructor of the class it builds: pass the same dependencies you would hand the new expression, and receive the fully typed instance back.


EntityFactory

A factory type that builds a fully-formed domain entity from its create input. Use cases such as CreateEntityUseCase accept one so they never need to know how a concrete entity is constructed. Its second generic, InputType, is the shape of that input — whatever your create DTO defines, independent of the entity's own schema.

import type { EntityFactory } from "@roastery/pantry/domain/types";

// (data, initialProperties?) => Entity
const createPost: EntityFactory<Post, t.Static<typeof CreatePostDTO>> = (data, initial) =>
  new Post(data, initial);

// Generates id / createdAt / updatedAt itself:
const post = createPost({ title: "Hello", slug: "hello" });

// Or preserves base fields when rebuilding a persisted entity:
const rebuilt = createPost({ title: "Hello", slug: "hello" }, { id, createdAt, updatedAt });

data is the create input (the InputType generic) — whatever shape your create DTO defines. The optional initialProperties supplies the base entity fields (id / createdAt / updatedAt) when you need to preserve them (e.g. rebuilding a persisted entity) instead of generating fresh ones.


SlugUniquenessCheckerService

Domain service that checks whether a slug is still available. It reads through any ICanFindBy source, so it is generic over a single EntityType.

import { SlugUniquenessCheckerService } from "@roastery/pantry/domain/services";

const checker = new SlugUniquenessCheckerService(reader); // ICanFindBy source

await checker.run("my-post");       // true  (unique — no entity uses it)
await checker.run("existing-post"); // false (already taken)

DTOs

Schema-validated Data Transfer Objects for common query parameters.

IdOrSlugDTO

Accepts either a UUID or a slug under the id-or-slug key:

import { IdOrSlugDTO } from "@roastery/pantry/presentation/dtos";

// { "id-or-slug": "550e8400-e29b-41d4-a716-446655440000" }
// { "id-or-slug": "my-cool-post" }

PaginationDTO

A 1-based page parameter (minimum 1, defaults to 1):

import { PaginationDTO } from "@roastery/pantry/presentation/dtos";

// { page: 1 }

baristaResponse

Builds the HTTP-status-to-schema response map used to document a Barista route handler. It merges your explicit success/error schemas with auto-generated error payloads for the exceptions a route may raise, resolving each exception to its corresponding HTTP status.

import { baristaResponse } from "@roastery/pantry/presentation/utils";

const responses = baristaResponse(
  { 200: PostSchema },
  { domain: ["ResourceNotFoundException"] },
);

// {
//   200: PostSchema,
//   404: <generated ResourceNotFoundException schema>,
// }

Status keys may be numeric (200) or names ("OK", "NotFound"); names are normalized to codes via HTTP_STATUS.


generateExceptionDTO

Factory that builds the schema describing the standard exception-response payload — message, name, source, and layer — pre-filling the example fields so generated API docs reflect the specific exception.

import { generateExceptionDTO } from "@roastery/pantry/presentation/dtos/utils";

const schema = generateExceptionDTO("ResourceNotFoundException", "domain");

Utilities

detectEntry

Detects whether a string is a UUID or a slug (via UuidSchema.match from @roastery/beans):

import { detectEntry } from "@roastery/pantry/application/utils";

detectEntry("550e8400-e29b-41d4-a716-446655440000"); // "UUID"
detectEntry("my-cool-post");                          // "SLUG"

getNumberOfPages

Computes how many pages are needed to paginate a collection. itemsPerPage defaults to MAX_ITEMS_PER_QUERY. Returns 0 when there is nothing to paginate.

import { getNumberOfPages } from "@roastery/pantry/application/utils";

getNumberOfPages(100, 10); // 10
getNumberOfPages(101, 10); // 11
getNumberOfPages(100);     // 8 (default: 14 items per page)
getNumberOfPages(0, 10);   // 0

Constants

import { MAX_ITEMS_PER_QUERY, CACHE_EXPIRATION_TIME } from "@roastery/pantry/constants";

| Constant | Value | Description | |----------|-------|-------------| | MAX_ITEMS_PER_QUERY | 14 | Default items per page for pagination | | CACHE_EXPIRATION_TIME.SAFE | 3600 | 1 hour (seconds) | | CACHE_EXPIRATION_TIME.LOW_UPDATES | 86400 | 24 hours (seconds) | | CACHE_EXPIRATION_TIME.HIGH_UPDATES | 900 | 15 minutes (seconds) |


Shared types

Application-layer types re-exported from a single barrel:

import type { ValueTypes, EntryActions, ICountItems } from "@roastery/pantry/application/types";

| Type | Description | |------|-------------| | ValueTypes | "UUID" \| "SLUG" — discriminates whether an identifier is a UUID or a slug. | | EntryActions<T> | Record<ValueTypes, () => T> — maps each entry kind to a handler producing a T. | | ICountItems | { totalPages: number; count: number } — pagination counters returned by list queries. |


Exports reference

// Application — Use Cases
import {
  FindEntityByTypeUseCase,
  FindManyEntitiesUseCase,
  CreateEntityUseCase,
  CreateSluggedEntityUseCase,
  UpdateEntityUseCase,
  DeleteEntityUseCase,
} from "@roastery/pantry/application/use-cases";

// Application — Types
import type { ValueTypes, EntryActions, ICountItems } from "@roastery/pantry/application/types";

// Application — Utilities
import { detectEntry, getNumberOfPages } from "@roastery/pantry/application/utils";

// Domain — Services
import { SlugUniquenessCheckerService } from "@roastery/pantry/domain/services";

// Domain — Types
import type { EntityFactory } from "@roastery/pantry/domain/types";

// Domain — Repository Contracts
import type {
  ICanCreate,
  ICanFind,
  ICanFindBy,
  ICanFindMany,
  ICanFindManyByIds,
  ICanUpdate,
  ICanDelete,
  ICanCount,
  IEntityReader,
  IEntityWriter,
  IEntityRepository,
} from "@roastery/pantry/domain/types/repositories";

// Presentation — DTOs
import { IdOrSlugDTO, PaginationDTO } from "@roastery/pantry/presentation/dtos";

// Presentation — DTO Utilities
import { generateExceptionDTO } from "@roastery/pantry/presentation/dtos/utils";

// Presentation — Utilities
import { baristaResponse } from "@roastery/pantry/presentation/utils";

// Constants
import { MAX_ITEMS_PER_QUERY, CACHE_EXPIRATION_TIME } from "@roastery/pantry/constants";

// Infra — Application Use-Case Factories
import {
  makeCreateEntityUseCase,
  makeCreateSluggedEntityUseCase,
  makeFindEntityByTypeUseCase,
  makeFindManyEntitiesUseCase,
  makeUpdateEntityUseCase,
  makeDeleteEntityUseCase,
} from "@roastery/pantry/infra/factories/application/use-cases";

// Infra — Domain Service Factories
import { makeSlugUniquenessCheckerService } from "@roastery/pantry/infra/factories/domain/services";

Development

# Run tests
bun run test:unit

# Run tests with coverage
bun run test:coverage

# Build for distribution
bun run build

# Check for unused exports and dependencies
bun run knip

# Full setup (build + bun link)
bun run setup

Changelog

See CHANGELOG.md for the release history.

License

MIT