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

tanstack-effect

v0.1.0

Published

React + Tanstack Query + Effect Fullstack e2e queries with validation

Readme

npm latest package Build Status License npm downloads Follow on Twitter

Tanstack Effect

Bun + Npm + Typescript + Standard Version + Flat Config Linting + Husky + Commit / Release Pipeline OpenAPI + Swagger UI + Tanstack Query + Effect Schemas

Summary

This package contains < tanstack effect > for MG.

Check out the Changelog to see what changed in the last releases.

Install

bun add tanstack-effect

Install Bun ( bun is the default package manager for this project ( its optional ) ):

# Supported on macOS, Linux, and WSL
curl -fsSL https://bun.sh/install | bash
# Upgrade Bun every once in a while
bun upgrade

Usage

The library is designed for a typed server-client workflow using Effect's HttpApi.

  1. Use the client-safe hooks on the frontend
// You need to import the shared file for the routes to register in runtime
import './shared'

import {
  useEffectMutation,
  useEffectQuery,
  useSchemaForm,
} from 'tanstack-effect/client'

import { UserSchema } from './shared'

export default function Page() {
  const user = useEffectQuery(
    'user',
    'user',
    {
      path: {
        username: 'test',
      },
    },
    {
      includeCredentials: true,
      noCache: false,
    }
  )

  const form = useSchemaForm({
    schema: UserSchema,
    initialData: user.data,
  })

  const updateUser = useEffectMutation('user', 'updateUser', {
    onSuccess: () => {
      console.log('Updated User')
    },
  })

  const handleSubmit = () => {
    if (!form.data || !user.data?.username) return
    updateUser.mutate({
      path: {
        username: user.data.username,
      },
      payload: form.data,
    })
  }

  return (
    <div className="space-y-4">
      <h1>User: {user.data?.username}</h1>
      <h1>Update User</h1>
      {/* Use form.fields, form.data, form.updateField, form.validationErrors
          to build your own form UI. See https://www.npmjs.com/package/liquidcn for a FormBuilder example. */}
      <button onClick={handleSubmit}>Update</button>
    </div>
  )
}

Available client hooks:

  • useEffectQuery
  • useEffectInfiniteQuery
  • useEffectMutation
  • useSchemaForm (supports optional AI integration via ai config)

Import them from tanstack-effect/client. The main entry tanstack-effect is server-safe and used to build the typed client from your HttpApi definition.

Schema-driven forms (Form Builder + Hook)

Build forms directly from your Effect Schema:

  • useSchemaForm hook manages form state, validation, and field updates
  • For a complete FormBuilder UI implementation, see liquidcn

The hook provides form.fields, form.data, form.updateField, and form.validationErrors for building your own form UI.

AI-Powered Form Filling (Optional)

Use AI to fill forms from natural language descriptions. The AI extracts structured data from prompts and generates clarification questions for missing required fields.

Requirements: Install optional peer dependencies:

bun add ai @ai-sdk/google zod

Set your Google AI API key in your environment:

export GOOGLE_GENERATIVE_AI_API_KEY="your-key"

Server-side: Create an API route

// app/api/ai-form-fill/route.ts
import { createAIFormFillerHandler } from 'tanstack-effect'

const handler = createAIFormFillerHandler()

export const POST = handler

Client-side: Enable AI on useSchemaForm

Simply add the ai option to useSchemaForm to enable AI form filling:

import { useSchemaForm } from 'tanstack-effect/client'
import { Schema } from 'effect'

const ProjectSchema = Schema.Struct({
  projectName: Schema.String.pipe(
    Schema.annotations({ description: 'Name of the project' })
  ),
  projectType: Schema.Literal('web', 'mobile', 'desktop').pipe(
    Schema.annotations({ description: 'Type of project' })
  ),
  teamSize: Schema.Number.pipe(
    Schema.annotations({ description: 'Number of team members' })
  ),
})

function ProjectForm() {
  const form = useSchemaForm({
    schema: ProjectSchema,
    // Enable AI by providing the endpoint
    ai: {
      endpoint: '/api/ai-form-fill',
    },
  })

  return (
    <div>
      {/* AI chat input */}
      <input
        placeholder="Describe your project..."
        onKeyDown={(e) => {
          if (e.key === 'Enter' && form.ai) {
            form.ai.fill(e.currentTarget.value)
          }
        }}
      />

      {form.ai?.status === 'filling' && <p>AI is filling the form...</p>}

      {/* Show AI summary when complete */}
      {form.ai?.summary && <p>{form.ai.summary}</p>}

      {/* Clarification questions */}
      {form.ai?.clarifications.map((q) => (
        <div key={q.field}>
          <p>{q.question}</p>
          {q.options?.map((opt) => (
            <button
              key={opt.value}
              onClick={() => form.ai?.answer(q.field, opt.value)}
            >
              {opt.label}
            </button>
          ))}
        </div>
      ))}

      {/* Form data is available in form.data */}
      {form.data && <pre>{JSON.stringify(form.data, null, 2)}</pre>}
    </div>
  )
}

AI state and actions

When ai is configured, form.ai provides:

| Property | Type | Description | |----------|------|-------------| | status | 'idle' \| 'filling' \| 'clarifying' \| 'complete' \| 'error' | Current AI status | | messages | AIFormMessage[] | Conversation history | | clarifications | ClarificationQuestion[] | Pending questions for user | | summary | string \| null | Human-readable summary (e.g. "Filled 3 fields: projectName, projectType, teamSize") | | fill(prompt) | (prompt: string) => Promise<void> | Fill form from natural language | | answer(field, value) | (field: string, value: unknown) => Promise<void> | Answer a clarification | | ask(question) | (question: string) => Promise<string> | Ask a follow-up question | | reset() | () => void | Reset AI state and messages |

How it works

  1. User provides a natural language prompt: "Building a mobile app called TravelBuddy with 5 developers"
  2. AI extracts structured data: { projectName: "TravelBuddy", projectType: "mobile", teamSize: 5 }
  3. form.data is updated automatically with the filled values
  4. If required fields are missing, form.ai.clarifications contains questions
  5. User answers clarifications, AI fills remaining fields
  6. form.ai.summary shows what was filled (e.g. "Filled 3 fields: projectName, projectType, teamSize")

The AI respects your Effect Schema:

  • Enum/literal types are constrained to valid options
  • Field descriptions help the AI understand what data to extract
  • Required vs optional fields determine when clarifications are needed

Important: Schema Annotations with optionalWith

When using Schema.optionalWith() for optional fields with defaults, annotations must be placed on the inner Schema type, not on the optionalWith result. This is due to how Effect Schema handles PropertySignature annotations internally.

Correct pattern (annotations preserved):

import { Schema } from 'effect'

const MySchema = Schema.Struct({
  // ✅ Annotations on the inner Schema type - WORKS
  maxItems: Schema.optionalWith(
    Schema.Number.annotations({
      title: 'Max Items',
      description: 'Maximum number of items to process',
    }),
    { default: () => 50 }
  ),

  // ✅ Using a pre-defined annotated Schema - WORKS
  logLevel: Schema.optionalWith(LogLevel, { default: () => 'info' }),
})

Incorrect pattern (annotations lost):

const MySchema = Schema.Struct({
  // ❌ Annotations on optionalWith result - DOES NOT WORK
  maxItems: Schema.optionalWith(Schema.Number, {
    default: () => 50,
  }).annotations({
    title: 'Max Items',
    description: 'Maximum number of items to process',
  }),
})

This limitation exists because Schema.optionalWith() returns a PropertySignature, and calling .annotations() on a PropertySignature doesn't store annotations in the AST in an accessible way. By placing annotations on the inner Schema type, the annotations are preserved and can be extracted by the form builder.

  1. Define your API on the shared file and generate the client type
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from '@effect/platform'
import { Schema } from 'effect'
import { getTanstackEffectClient } from 'tanstack-effect'

// Base user schema - i.e. the schema which would be in the database
export const UserSchema = Schema.Struct({
  username: Schema.String,
  name: Schema.String,
  surname: Schema.String,
  email: Schema.String,
})

// Path params for the user endpoint
export const GetUserPathParams = Schema.Struct({
  username: Schema.String,
})

// Update user request - i.e. the schema which would be sent to the server
export const UpdateUserRequest = Schema.partial(UserSchema)

export const userGroup = HttpApiGroup.make('user')
  .add(
    HttpApiEndpoint.get('user', '/:username')
      .setPath(GetUserPathParams)
      .addSuccess(UserSchema)
  )
  .add(
    HttpApiEndpoint.put('updateUser', '/:username')
      .setPath(GetUserPathParams)
      .setPayload(UpdateUserRequest)
      .addSuccess(UserSchema)
  )
  .prefix('/user')

const ErrorResponse = Schema.Struct({
  message: Schema.String,
})

// Define the API contract
export const Api = HttpApi.make('Api')
  // Define global errors that apply to all endpoints
  .addError(ErrorResponse, { status: 400 }) // Bad Request
  .addError(ErrorResponse, { status: 401 }) // Unauthorized
  .addError(ErrorResponse, { status: 403 }) // Forbidden
  .addError(ErrorResponse, { status: 404 }) // Not Found
  .addError(ErrorResponse, { status: 500 }) // Internal Server Error
  .addError(ErrorResponse, { status: 503 }) // Service Unavailable
  .addError(ErrorResponse, { status: 504 }) // Gateway Timeout
  .addError(ErrorResponse, { status: 429 }) // Too Many Requests
  .addError(ErrorResponse, { status: 405 }) // Method Not Allowed
  .addError(ErrorResponse, { status: 406 }) // Not Acceptable
  .addError(ErrorResponse, { status: 408 }) // Request Timeout
  .addError(ErrorResponse, { status: 409 }) // Conflict
  .add(userGroup)

export class TanstackEffectClient extends getTanstackEffectClient(Api) {}

export type TTanstackEffectClient = TanstackEffectClient['client']
  1. Augment the tanstack-effect client interface in a .d.ts

Place a declaration file accessible to your app (e.g. src/types/tanstack-effect.d.ts) and ensure your tsconfig.json includes it.

import type { TTanstackEffectClient as Client } from './shared'

declare module 'tanstack-effect' {
  interface TTanstackEffectClient extends Client {}
}
  1. Set up the route for the API (required)
import type { GetCleanSuccessType, GetRequestParams } from 'tanstack-effect'

// Mock Hono class / for demonstration purposes
class Hono {
  [key: string]: any
}

// Minimal mock server to serve the OpenAPI spec
const app = new Hono()

// Get user route like its defined in the schema
app.get('/user/:username', async (c: any) => {
  const { username } = c.req.param()

  // Some function to get the user
  const request = async (
    params: GetRequestParams<'user', 'user'>
  ): Promise<GetCleanSuccessType<'user', 'user'>> => {
    // Some logic to get the user
    return {} as any
  }

  const user = await request({
    path: {
      username,
    },
  })

  return c.json(user)
})

// Update user route like its defined in the schema
app.put('/user/:username', async (c: any) => {
  const { username } = c.req.param()
  const body: GetRequestParams<'user', 'updateUser'>['payload'] =
    await c.req.json()

  // Some function to update the user
  const request = async (
    params: GetRequestParams<'user', 'updateUser'>
  ): Promise<GetCleanSuccessType<'user', 'updateUser'>> => {
    // Some logic to update the user
    return {} as any
  }

  const updatedUser = await request({
    path: {
      username,
    },
    payload: body,
  })

  return c.json(updatedUser)
})

export default app
  1. Set up OpenAPI documentation (optional)
import { OpenApi } from '@effect/platform'

// Importing TanstackEffectClient to mirror real-world usage where this is the API import equivalent
import { Api } from './shared'

// Mock Hono class / for demonstration purposes
class Hono {
  [key: string]: any
}

// Minimal mock server to serve the OpenAPI spec
const app = new Hono()

app.get('/docs/openapi.json', (c: any) => {
  const spec = OpenApi.fromApi(Api)
  return c.json(spec)
})

app.get('/docs', (c: any) =>
  c.html(`
    <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="description" content="SwaggerUI" />
  <title>SwaggerUI</title>
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js" crossorigin></script>
<script>
  window.onload = () => {
    window.ui = SwaggerUIBundle({
      url: '/docs/openapi.json',
      dom_id: '#swagger-ui',
    });
  };
</script>
</body>
</html>`)
)

export default {
  port: 8080,
  fetch: app.fetch,
}

Developing

Install Dependencies:

bun i

Watching TS Problems:

bun watch

Format / Lint / Type Check:

bun format
bun lint
bun type-check

How to make a release

For the Maintainer: Add NPM_TOKEN to the GitHub Secrets.

  1. PR with changes
  2. Merge PR into main
  3. Checkout main
  4. git pull
  5. bun release: '' | alpha | beta optionally add -- --release-as minor | major | 0.0.1
  6. Make sure everything looks good (e.g. in CHANGELOG.md)
  7. Lastly run bun release:pub
  8. Done

License

This package is licensed - see the LICENSE file for details.