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

@pyreon/feature

v0.10.0

Published

Schema-driven feature primitives — define once, get CRUD hooks, forms, tables, and stores

Downloads

1,604

Readme

@pyreon/feature

Schema-driven feature primitives for Pyreon. Define a Zod schema and API path once, get fully typed CRUD hooks, forms, tables, stores, pagination, optimistic updates, and references -- all wired together automatically.

Install

bun add @pyreon/feature

Peer dependencies: @pyreon/core, @pyreon/reactivity

Quick Start

import { defineFeature, reference } from '@pyreon/feature'
import { z } from 'zod'

const users = defineFeature({
  name: 'users',
  schema: z.object({
    name: z.string().min(2),
    email: z.string().email(),
    role: z.enum(['admin', 'editor', 'viewer']),
  }),
  api: '/api/users',
})

// List query
function UserList() {
  const { data, isPending } = users.useList()
  if (isPending()) return <p>Loading...</p>
  return (
    <ul>
      {data()!.map((u) => (
        <li>{u.name} ({u.email})</li>
      ))}
    </ul>
  )
}

// Create form
function CreateUser() {
  const form = users.useForm()
  return (
    <form onSubmit={(e) => form.handleSubmit(e)}>
      <input {...form.register('name')} />
      <input {...form.register('email')} />
      <select {...form.register('role')}>
        <option value="admin">Admin</option>
        <option value="editor">Editor</option>
        <option value="viewer">Viewer</option>
      </select>
      <button type="submit">Create</button>
    </form>
  )
}

// Edit form (auto-fetches data)
function EditUser({ id }: { id: number }) {
  const form = users.useForm({ mode: 'edit', id })
  if (form.isSubmitting()) return <p>Loading...</p>
  return (
    <form onSubmit={(e) => form.handleSubmit(e)}>
      <input {...form.register('name')} />
      <input {...form.register('email')} />
      <button type="submit">Save</button>
    </form>
  )
}

API Reference

defineFeature(config)

Creates a feature object with all CRUD hooks, form, table, and store.

| Parameter | Type | Description | | --- | --- | --- | | name | string | Unique feature name -- used for store ID and query key namespace | | schema | ZodSchema | Validation schema -- passed to zodSchema() for form validation | | api | string | API base path (e.g., /api/users) | | initialValues? | Partial<TValues> | Default values for create forms (auto-generated from schema if omitted) | | validate? | SchemaValidateFn<TValues> | Custom schema-level validation (overrides auto-detection) | | fetcher? | typeof fetch | Custom fetch function (defaults to global fetch) |

Returned Feature Object

| Property / Hook | Returns | Description | | --- | --- | --- | | name | string | Feature name | | api | string | API base path | | schema | unknown | The schema passed to defineFeature | | fields | FieldInfo[] | Introspected field metadata from the schema | | queryKey(suffix?) | QueryKey | Generate namespaced query keys | | useList(opts?) | UseQueryResult<T[]> | GET api -- list query with optional pagination and params | | useById(id) | UseQueryResult<T> | GET api/:id -- single item query | | useSearch(term, opts?) | UseQueryResult<T[]> | GET api?q=term -- search with reactive signal | | useCreate() | UseMutationResult | POST api -- invalidates list on success | | useUpdate() | UseMutationResult | PUT api/:id -- optimistic update with rollback on error | | useDelete() | UseMutationResult | DELETE api/:id -- invalidates list on success | | useForm(opts?) | FormState<T> | Form with schema validation + API submit | | useTable(data, opts?) | FeatureTableResult<T> | Reactive table with schema-inferred columns | | useStore() | StoreApi<FeatureStore<T>> | Reactive store for items, selection, and loading state |

reference(feature)

Creates a typed foreign key field for cross-feature relationships.

| Parameter | Type | Description | | --- | --- | --- | | feature | { name: string } | The referenced feature (or any object with a name property) |

Returns a Zod-compatible schema that validates as string | number and carries metadata about the referenced feature.

extractFields(schema)

Extracts field metadata from a Zod object schema.

| Parameter | Type | Description | | --- | --- | --- | | schema | unknown | A Zod object schema (duck-typed, works with v3 and v4) |

Returns FieldInfo[] with name, type, optional, enumValues, referenceTo, and label for each field.

isReference(value)

Returns true if a value is a reference schema created by reference().

defaultInitialValues(fields)

Generates default initial values from a FieldInfo[] array. Strings default to '', numbers to 0, booleans to false, enums to the first value.

useStore

The feature store provides a reactive cache for list data and selection state. It uses @pyreon/store internally with the feature name as the store ID.

function UserManager() {
  const { store } = users.useStore()
  const { data } = users.useList()

  // Sync query data to store
  effect(() => {
    const items = data()
    if (items) store.items.set(items)
  })

  return (
    <div>
      <ul>
        {store.items().map((u) => (
          <li onClick={() => store.select(u.id)}>
            {u.name}
          </li>
        ))}
      </ul>
      {store.selected() && (
        <div>Selected: {store.selected()!.name}</div>
      )}
      <button onClick={() => store.clear()}>Clear Selection</button>
    </div>
  )
}

Store API:

| Property | Type | Description | | --- | --- | --- | | items | Signal<TValues[]> | Cached list of items | | selected | Signal<TValues \| null> | Currently selected item | | loading | Signal<boolean> | Loading state | | select(id) | (id: string \| number) => void | Find and select an item by ID from the items list | | clear() | () => void | Clear the current selection |

Pagination

Pass page (number or reactive signal) and pageSize to useList() for automatic pagination. Each page is cached independently.

function PaginatedUsers() {
  const page = signal(1)
  const { data, isPending } = users.useList({ page, pageSize: 10 })

  return (
    <div>
      {isPending() ? (
        <p>Loading...</p>
      ) : (
        <ul>
          {data()!.map((u) => <li>{u.name}</li>)}
        </ul>
      )}
      <button onClick={() => page.set(page() - 1)} disabled={page() <= 1}>
        Previous
      </button>
      <button onClick={() => page.set(page() + 1)}>
        Next
      </button>
    </div>
  )
}

ListOptions:

| Parameter | Type | Description | | --- | --- | --- | | params? | Record<string, string \| number \| boolean> | Additional query parameters | | page? | number \| Signal<number> | Page number (reactive or static) | | pageSize? | number | Items per page (defaults to 20 when page is set) | | staleTime? | number | Override stale time for this query | | enabled? | boolean | Enable/disable the query |

Edit Form (Auto-fetch)

When useForm() is called with mode: 'edit' and an id, it automatically fetches the item and populates the form. The form's isSubmitting signal is true until the data loads.

function EditUser({ id }: { id: number }) {
  const form = users.useForm({
    mode: 'edit',
    id,
    onSuccess: () => console.log('Updated!'),
    onError: (err) => console.error(err),
  })

  if (form.isSubmitting()) return <p>Loading user...</p>

  return (
    <form onSubmit={(e) => form.handleSubmit(e)}>
      <input {...form.register('name')} />
      <input {...form.register('email')} />
      <button type="submit">Save</button>
    </form>
  )
}

FeatureFormOptions:

| Parameter | Type | Description | | --- | --- | --- | | mode? | 'create' \| 'edit' | Form mode (default: 'create') | | id? | string \| number | Item ID for edit mode (triggers auto-fetch) | | initialValues? | Partial<TValues> | Override initial values | | validateOn? | 'blur' \| 'change' \| 'submit' | Validation trigger (default: 'blur') | | onSuccess? | (result: unknown) => void | Called after successful submit | | onError? | (error: unknown) => void | Called on submit error |

Optimistic Updates

useUpdate() automatically performs optimistic cache updates. When a mutation starts, the query cache is updated immediately with the new data. If the server returns an error, the cache rolls back to the previous value.

function UserRow({ user }: { user: User }) {
  const { mutate: update } = users.useUpdate()

  const toggleActive = () => {
    // Cache updates immediately, rolls back on error
    update({ id: user.id, data: { active: !user.active } })
  }

  return (
    <tr>
      <td>{user.name}</td>
      <td>
        <button onClick={toggleActive}>
          {user.active ? 'Deactivate' : 'Activate'}
        </button>
      </td>
    </tr>
  )
}

References

Use reference() to define typed foreign keys between features. Reference fields validate as string | number and carry metadata for form dropdowns and table links.

import { defineFeature, reference } from '@pyreon/feature'
import { z } from 'zod'

const users = defineFeature({
  name: 'users',
  schema: z.object({ name: z.string(), email: z.string().email() }),
  api: '/api/users',
})

const posts = defineFeature({
  name: 'posts',
  schema: z.object({
    title: z.string(),
    body: z.string(),
    authorId: reference(users),  // typed foreign key
  }),
  api: '/api/posts',
})

// Field introspection detects the reference
const authorField = posts.fields.find((f) => f.name === 'authorId')
// { name: 'authorId', type: 'reference', referenceTo: 'users', ... }

Schema Introspection

Every feature exposes fields: FieldInfo[] with metadata extracted from the schema at runtime. This powers automatic table columns, form generation, and reference detection.

function AutoForm({ feature }: { feature: Feature<any> }) {
  const form = feature.useForm()
  return (
    <form onSubmit={(e) => form.handleSubmit(e)}>
      {feature.fields.map((field) => {
        if (field.type === 'enum') {
          return (
            <select {...form.register(field.name)}>
              {field.enumValues!.map((v) => (
                <option value={v}>{v}</option>
              ))}
            </select>
          )
        }
        if (field.type === 'boolean') {
          return <input type="checkbox" {...form.register(field.name, { type: 'checkbox' })} />
        }
        if (field.type === 'reference') {
          return <p>Reference to: {field.referenceTo}</p>
        }
        return <input {...form.register(field.name)} placeholder={field.label} />
      })}
      <button type="submit">Submit</button>
    </form>
  )
}

FieldInfo:

| Property | Type | Description | | --- | --- | --- | | name | string | Field name (key in the schema) | | type | FieldType | 'string', 'number', 'boolean', 'date', 'enum', 'array', 'object', 'reference', or 'unknown' | | optional | boolean | Whether the field is optional | | enumValues? | (string \| number)[] | Allowed values for enum fields | | referenceTo? | string | Referenced feature name for reference fields | | label | string | Human-readable label (e.g., firstName becomes First Name) |

Error Handling

The built-in fetcher parses structured error responses from the API. Errors with a message field in the response body use that as the error message. Errors with an errors object attach it to the thrown error for field-level error handling.

function CreateUser() {
  const { mutate, error, isError } = users.useCreate()

  const handleCreate = () => {
    mutate({ name: 'Alice', email: '[email protected]' })
  }

  return (
    <div>
      <button onClick={handleCreate}>Create</button>
      {isError() && (
        <div>
          <p>{(error() as Error).message}</p>
          {(error() as any).errors?.email && (
            <p>Email: {(error() as any).errors.email}</p>
          )}
        </div>
      )}
    </div>
  )
}

Why

An AI agent asked to "add user management" writes 10 lines of schema instead of 200 lines of components, hooks, and wiring. The feature definition is the single source of truth -- types, validation, API calls, cache management, optimistic updates, pagination, and store state all flow from the schema. Human developers get the same leverage: one defineFeature() call replaces dozens of boilerplate files.