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

ggtype

v0.6.4

Published

High-performance TypeScript library for type-safe data validation and client-server communication with bidirectional RPC, streaming support, and multiple transport options

Readme

ggtype

Build Code Quality Check Build Size

Type-safe bidirectional RPC with automatic validation and full TypeScript inference.

ggtype is a high-performance TypeScript library for building type-safe communication between client and server. Define your API once, get automatic runtime validation, full type inference, and bidirectional RPC—all with zero code generation.


What problem does this solve?

  • Type safety without duplication — No need to maintain separate types for client and server. Define once, use everywhere with full inference.
  • Runtime validation that matches your types — Automatic validation using AJV ensures your runtime data matches your TypeScript types.
  • Bidirectional RPC with validation — Server can call client actions (and vice versa) with full type safety and validation on both sides.

When should I use ggtype?

Use ggtype when:

  • You're building a full-stack TypeScript application
  • You want type-safe API calls without code generation
  • You need bidirectional communication (server calling client)
  • You want automatic runtime validation

Features

  • 🎯 Full TypeScript inference — Types flow automatically from server to client, no manual type definitions
  • Automatic validation — Runtime validation using AJV ensures data matches your types
  • 🔄 Bidirectional RPC — Server can call client actions with full type safety and validation
  • 📡 Multiple transports — HTTP, streaming, WebSocket, and half-duplex—use what fits your needs
  • 🚀 High performance — Fast validation, parallel execution, efficient streaming
  • 🎨 Zero boilerplate — Simple API, works with any server framework
  • 📁 File support — Built-in file upload/download across all transports
  • 🛡️ Type-safe errors — Validation errors with detailed messages and type guards

Quick Start

1. Install

bun add ggtype
# or
npm install ggtype
# or
yarn add ggtype

2. Define your server

// server.ts
import { action, createRouter, m } from 'ggtype'

// Define an action with validation
const getUser = action(
  m.object({ id: m.string() }),
  async ({ params }) => {
    return { 
      id: params.id, 
      name: 'John Doe', 
      email: '[email protected]' 
    }
  }
)

// Create router
const router = createRouter({
  serverActions: { getUser },
})

export type Router = typeof router

// Use with any server
Bun.serve({
  port: 3000,
  async fetch(request) {
    return router.onRequest({ request, ctx: {} })
  },
})

3. Use in your client

// client.ts
import { createRouterClient, isSuccess } from 'ggtype'
import type { Router } from './server' // Type-only import

const client = createRouterClient<Router>({
  httpURL: 'http://localhost:3000',
})

// Call with full type safety
const result = await client.fetchActions.getUser({ id: '1' })

if (isSuccess(result)) {
  console.log('User:', result.data) // Fully typed!
} else {
  console.error('Error:', result.error?.message)
}

That's it! You get automatic validation, full TypeScript types, and a simple API.


Examples

For complete, runnable examples, see the examples/ folder:

  • Hello World — Basic type-safe communication
  • AI Tools — Bidirectional RPC (server calls client)
  • Streaming — Real-time streaming updates
  • WebSocket — Persistent bidirectional connections
  • Duplex — Interactive bidirectional streaming

Core Concepts

Actions

Actions are type-safe functions with automatic validation:

const createUser = action(
  m.object({
    name: m.string(),
    email: m.string().isEmail(),
    age: m.number().min(18),
  }),
  async ({ params }) => {
    // params is fully typed and validated
    return { id: '123', ...params }
  }
)

Routers

Routers organize your actions and enable bidirectional RPC:

const router = createRouter({
  serverActions: { getUser, createUser },
  clientActions: { /* optional */ },
})

Clients

Clients provide type-safe access to your server actions:

const client = createRouterClient<Router>({
  httpURL: 'http://localhost:3000',
})

// Use proxy methods for automatic type narrowing
const result = await client.fetchActions.getUser({ id: '1' })

Streaming

Return streams from actions for real-time data:

// server.ts
const searchUsers = action(
  m.object({ query: m.string() }),
  async function* ({ params }) {
    // Yield results as they become available
    yield { id: '1', name: 'John', query: params.query }
    yield { id: '2', name: 'Jane', query: params.query }
  }
)

// client.ts
const stream = client.streamActions.searchUsers({ query: 'john' })

for await (const result of stream) {
  if (isSuccess(result)) {
    console.log('User:', result.data)
  }
}

Bidirectional RPC

The killer feature: Server can call client actions with full type safety and validation.

// server.ts
const clientActions = defineClientActionsSchema({
  showNotification: {
    params: m.object({ message: m.string() }),
    return: m.object({ acknowledged: m.boolean() }),
  },
})

type ClientActions = typeof clientActions

const updateUser = action(
  m.object({ id: m.string(), name: m.string() }),
  async ({ params, clientActions }) => {
    // Call client action with type safety
    const { showNotification } = clientActions<ClientActions>()
    const result = await showNotification?.({
      message: `User ${params.id} updated!`,
    })
    
    // Client response is validated automatically!
    if (result?.status === 'ok') {
      console.log('Acknowledged:', result.data.acknowledged)
    }
    
    return { success: true }
  }
)

const router = createRouter({
  serverActions: { updateUser },
  clientActions, // Enable bidirectional RPC
})
// client.ts
const client = createRouterClient<Router>({
  streamURL: 'http://localhost:3000', // Use stream/websocket for bidirectional
  defineClientActions: {
    showNotification: async (params) => {
      // params is validated automatically
      alert(params.message)
      // Return value is validated against schema
      return { acknowledged: true }
    },
  },
})

Key benefits:

  • ✅ Full type safety on both sides
  • ✅ Client responses are validated automatically
  • ✅ Works with streaming, WebSocket, and duplex transports
  • ✅ No manual type definitions needed

Transports

ggtype supports multiple transports. Choose what fits your needs:

| Transport | Use Case | Bidirectional RPC | |-----------|----------|-------------------| | httpURL | Simple request/response (REST-like) | ❌ | | streamURL | HTTP streaming, real-time updates | ✅ | | websocketURL | Persistent connections, chat, games | ✅ | | halfDuplexUrl | Interactive bidirectional streaming | ✅ |

Transport selection: When multiple URLs are provided, the client uses the first available in priority order (stream → websocket → http). No automatic downgrade.

const client = createRouterClient<Router>({
  streamURL: 'http://localhost:3000/stream',    // Tried first
  websocketURL: 'ws://localhost:3000/ws',       // Fallback
  httpURL: 'http://localhost:3000/http',        // Last resort
})

Server Integration

Works with any server framework. Just plug in onRequest or onMessage:

// Bun
Bun.serve({
  async fetch(request) {
    return router.onRequest({ request, ctx: {} })
  },
})

// WebSocket
Bun.serve({
  websocket: {
    message(ws, message) {
      router.onWebSocketMessage({
        ws,
        message: message as Uint8Array,
        ctx: {},
      })
    },
  },
})

// Elysia, Express, etc.
app.post('/api', async (req, res) => {
  const response = await router.onRequest({
    request: req,
    ctx: {},
  })
  return response
})

Error Handling

ggtype provides type-safe error handling:

import { isSuccess, isValidationError } from 'ggtype'

const result = await client.fetchActions.getUser({ id: '123' })

if (isSuccess(result)) {
  // TypeScript knows result.data exists
  console.log('User:', result.data)
} else {
  // TypeScript knows result.error exists
  if (isValidationError(result.error)) {
    console.error('Validation errors:', result.error.errors)
  } else {
    console.error('Error:', result.error.message)
  }
}

File Upload/Download

Built-in file support across all transports:

// Server: Receive files
const uploadImage = action(
  m.object({ title: m.string() }),
  async ({ params, files }) => {
    const imageFile = files?.get('file')
    // Process file...
    return { success: true }
  }
)

// Client: Upload files
const result = await client.fetchActions.uploadImage(
  { title: 'My Image' },
  { files: [imageFile] }
)

// Server: Return files
const getFile = action(
  m.object({ id: m.string() }),
  async ({ params }) => {
    return new File([content], 'document.pdf', {
      type: 'application/pdf',
    })
  }
)

// Client: Receive files
const result = await client.fetchActions.getFile({ id: '123' })
if (isSuccess(result)) {
  const file = result.data // File object
  const url = URL.createObjectURL(file)
}

Why ggtype vs alternatives?

vs REST/GraphQL

  • Full type safety — End-to-end TypeScript inference
  • Automatic validation — Runtime validation matches your types
  • Bidirectional — Server can call client, not just client→server
  • Less boilerplate — No manual API definitions or code generation

vs WebSocket libraries

  • Type-safe — Full TypeScript inference for all messages
  • Validation — Automatic validation on both sides
  • Multiple transports — Not just WebSocket, choose what fits
  • Simple API — Clean, consistent API across all transports

Model System

Rich validation system with TypeScript inference:

// Primitives
m.string(), m.number(), m.boolean(), m.date(), m.null()

// Files
m.file(), m.blob()

// Collections
m.array(model), m.object({ ... }), m.record(model)

// Unions
m.or(model1, model2), m.and(model1, model2), m.enums('a', 'b', 'c')

// Constraints
m.string().min(5).max(100).pattern(/^[A-Z]/)
m.number().min(0).max(100)
m.string().isEmail().isOptional()

// Custom validation
m.string().validate((value) => {
  if (value.length < 5) {
    return 'Must be at least 5 characters'
  }
})

All models are required by default. Use .isOptional() to make them optional.


API Overview

Router

createRouter(options)
  - serverActions: Record<string, Action>
  - clientActions?: ClientActionsSchema
  - responseTimeout?: number

router.onRequest(options)      // HTTP requests
router.onStream(options)       // HTTP streaming
router.onWebSocketMessage(options) // WebSocket messages

Client

createRouterClient<Router>(options)
  - httpURL?: string
  - streamURL?: string
  - websocketURL?: string
  - halfDuplexUrl?: string
  - defineClientActions?: Record<string, Function>
  - defaultHeaders?: Headers

client.fetch(params, options?)        // Multiple actions
client.stream(params, options?)       // Stream multiple actions
client.fetchActions.actionName(...)   // Single action (proxy)
client.streamActions.actionName(...)  // Stream single action (proxy)
client.startWebsocket()               // WebSocket connection
client.startDuplex()                  // Duplex connection

Actions

action(model, callback)
  - callback receives: { params, ctx, clientActions, files }
  - action.run(params) - Test actions directly (clientActions is optional)

defineClientActionsSchema(schema)

Utilities

isSuccess(result)           // Type guard for success
isError(result)             // Type guard for error
isValidationError(error)    // Type guard for validation errors
ValidationError             // Error class

📚 Full API Documentation — Complete reference with detailed examples and type definitions.


Advanced

Optional Parameters

const getUser = action(
  m.object({
    id: m.string(),
    includeEmail: m.boolean().isOptional(),
  }),
  async ({ params }) => {
    // params.includeEmail is optional
  }
)

Context

Pass context to actions:

const router = createRouter({
  serverActions: { getUser },
})

router.onRequest({
  request,
  ctx: { user: currentUser }, // Pass context
})

// In action
const getUser = action(
  m.object({ id: m.string() }),
  async ({ params, ctx }) => {
    const user = ctx?.user
    // Use context
  }
)

Response Timeout

const router = createRouter({
  serverActions: { getUser },
  responseTimeout: 30000, // 30 seconds
})

Testing Actions

You can test actions directly using action.run(). The clientActions parameter is optional—if not provided, it defaults to an empty object:

import { action, m } from 'ggtype'

const getUser = action(
  m.object({ id: m.string() }),
  async ({ params, ctx, clientActions }) => {
    // clientActions() is always available, even when not provided in tests
    const actions = clientActions()
    return { id: params.id, name: 'John' }
  }
)

// Test without clientActions (optional)
const result = await getUser.run({
  params: { id: '123' },
  ctx: { userId: 'user-1' },
  // clientActions is optional - defaults to empty object
})

// Test with custom clientActions
const resultWithClientActions = await getUser.run({
  params: { id: '123' },
  ctx: { userId: 'user-1' },
  clientActions: () => ({
    showNotification: async () => ({ status: 'ok', data: { acknowledged: true } }),
  }),
})

Performance

  • Fast validation — Uses AJV (faster than Zod)
  • 🚀 Parallel execution — Multiple actions run in parallel
  • 💾 Efficient streaming — Minimal memory overhead
  • 📦 Lightweight — Minimal dependencies

Resources


License

MIT License — see LICENSE file for details.