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

railiz-rpc

v1.1.0

Published

Lightweight, adapter-first, type-safe RPC framework inspired by tRPC

Readme

🌐 railiz-rpc

NPM Downloads Bundle Size

LIVE EXAMPLE

🚀 Lightweight tRPC-style RPC framework built on Railiz engine

Built for people who want tRPC DX but don’t want tRPC complexity.


Why railiz-rpc?

  • No router boilerplate
  • No REST endpoints
  • No schema duplication
  • Works on Edge, Node, Serverless
  • Plugin-first architecture
rpc.user.get({ id: 1 })

What is railiz-rpc?

  • Type-safe RPC like tRPC
  • Multi-router architecture
  • Edge / Node / Serverless compatible
  • Plugin system (auth, logger, cache)
  • React Query hooks generator
  • OpenAPI generator built-in

👉 Minimal core
👉 Maximum flexibility


Installation

npm install railiz-rpc railiz zod

Mental Model

Client → Transport → RPC Handler → Router → Procedure → Middleware → Response

Everything is:

  • fully typed
  • composable
  • framework-agnostic

Quick Start

import { json, Railiz } from 'railiz'
import { createProcedure, createRouter, createRPC } from 'railiz-rpc'
import { rpcClient, rpcHandler } from 'railiz-rpc/transport'
import { z } from 'zod'

const app = new Railiz()

type AppContext = {
  headers: Record<string, string>
}

const t = createProcedure<AppContext>()

const userRouter = createRouter({
  get: t
    .input(z.object({ id: z.number() }))
    .resolve(({ input }) => {
      return { id: input.id, name: 'Tung' }
    }),
})

const rpc = createRPC({
  user: userRouter,
})

app.use(json())

app.plugin(
  rpcHandler(rpc, {
    prefix: '/rpc',
    // rpc plugin
    plugins: [
      loggerPlugin(),
      // cachePlugin({ ttl: 5000 })
    ],
  }),
)

// use from client
export type AppRouter = typeof rpc

app.run(3000)

Advanced Example (Auth + Cache + React)

// assume auth + cache plugin already configured
const userRouter = createRouter({
  get: t
    .input(z.object({ id: z.number() }))
    .resolve(({ input }) => {
      return { id: input.id }
    }),

  me: t.use(auth).resolve(({ context }) => {
    return context.user
  }),

  list: t.resolve(() => {
    return [
      { id: 1 },
      { id: 2 },
    ]
  }),
})

// Cache moved to RPC plugin (recommended)
app.plugin(
  rpcHandler(rpc, {
    prefix: '/rpc',
    cache: {
      enabled: true,
      ttl: 5000,
      key: ({ path, input, context }) => {
        return `${path}:${context?.state?.user?.id ?? 'anon'}`
      },
      condition: ({ path, input }) => {
        // better than string matching
        return path === 'user.list'
      },
    },
  }),
)

Client:

import { rpcClient, rpcHandler } from 'railiz-rpc/transport'
import type { AppRouter } from 'yourbackend'

const rpc = rpcClient<AppRouter>('/rpc')

await rpc.user.get({ id: 1 })
await rpc.user.me()
await rpc.user.list()

React:

const { data } = rpc.user.get.useQuery({ id: 1 })

React Query Proxy Client

Example usage of rpcReactClient to create a type-safe client with automatic React Query hooks.


Server Router


const userRouter = createRouter({
  get: t
    .input(z.object({ id: z.number() }))
    .resolve(({ input }) => {
      return {
        id: input.id,
        name: 'Tung',
      }
    }),
})

const rpc = createRPC({
  user: userRouter,
})

Create React RPC Client

import { rpcReactClient } from 'railiz-rpc/transport'
import type { AppRouter } from 'yourbackend'

const client = rpcReactClient<AppRouter>({
  'user.get': async (input: { id: number }) => {
    const res = await fetch('/rpc/user.get', {
      method: 'POST',
      body: JSON.stringify(input),
    })

    return res.json()
  },
})
  • This is a low-level adapter example

Use in React (useQuery)

import { useQuery } from '@tanstack/react-query'

function UserPage() {
  const { queryKey, queryFn } = client.user.get.useQuery({ id: 1 })

  const { data } = useQuery({
    queryKey,
    queryFn,
  })

  return <div>{data?.name}</div>
}

Use Mutation

function UpdateUser() {
  const { mutate } = client.user.get.useMutation()

  return (
    <button
      onClick={async () => {
        const res = await mutate({ id: 1 })
        console.log(res)
      }}
    >
      Load user
    </button>
  )
}

Direct Call (optional fallback)

await client.user.get({ id: 1 })
rpcReactClient -> Proxy (user.get) -> useQuery / useMutation / direct call -> fetch transport -> railiz-rpc server

Multi Router

const rpc = createRPC({
  user: userRouter,
  post: postRouter,
})

Middleware

const auth = createAuthPlugin({
  validate: async (token) => {
    if (token === 'admin') return { id: 1 }
    return null
  },
})

OpenAPI

import { openApi } from 'railiz-rpc/openapi'

const spec = openApi(rpc)

Multi RPC Instances

app.plugin( rpcHandler(rpc, { prefix: '/rpc', }))

app.plugin( rpcHandler(rpc2, { prefix: '/rpc2', }))
  • Fully supported — isolate domains / services easily

Protocol Spec

All RPC calls are sent to a single endpoint:

POST /rpc

Request

Single call

{
  "path": "user.get",
  "input": { "id": 1 }
}

Batch call

[
  { "path": "user.get", "input": { "id": 1 } },
  { "path": "post.list", "input": {} }
]

Response

All responses are always wrapped in a standard envelope:

{
  "success": true,
  "data": "result"
}

HTTP Status Codes

  • 200 OK → RPC executed (even if some procedures fail)
  • 400 Bad Request → invalid request (e.g. empty body)

Error model

  • Errors are not thrown as HTTP errors.
  • Instead, they are returned inside the response:
{
  "success": true,
  "data": {
    "error": "RPC_ERROR"
  }
}

or in batch:

{
  "success": true,
  "data": [
    { "error": "USER_NOT_FOUND" },
    { "id": 1, "name": "Tung" }
  ]
}

Design Note

Railiz-rpc uses a single endpoint + RPC envelope model:

  • HTTP handles transport only
  • Business results are returned in data
  • Errors are normalized inside response payload

There is only one HTTP endpoint. All logic is resolved via RPC path.

HTTP status is always 200 unless request is invalid


Transport Layer (Adapters)

Use the same router across different runtimes.


Fetch (Edge / Cloudflare / Bun)

import { createFetchHandler } from 'railiz-rpc/adapter'

export default {
  fetch: createFetchHandler(rpc),
}

Express

import express from 'express'
import { createExpressHandler } from 'railiz-rpc/adapter'

const app = express()

app.use(express.json())

app.post('/rpc', createExpressHandler(rpc))

Fastify

import Fastify from 'fastify'
import { createFastifyHandler } from 'railiz-rpc/adapter'

const app = Fastify()

app.post('/rpc', createFastifyHandler(rpc))

Hono

import { Hono } from 'hono'
import { createHonoHandler } from 'railiz-rpc/adapter'

const app = new Hono()

app.post('/rpc', createHonoHandler(rpc))

Next.js (Route Handler)

import { createFetchHandler } from 'railiz-rpc/adapter'

export const POST = createFetchHandler(rpc)

WebSocket

import { createWebSocketHandler } from 'railiz-rpc/adapter'

const ws = createWebSocketHandler(rpc)

createApp (Advanced Server)

One function to bootstrap everything

Basic

import { createApp } from 'railiz-rpc/transport'

const app = createApp({ rpc })
app.run(3000)

Full Setup

import { createApp } from 'railiz-rpc/transport'

const app = createApp({
  rpc,

  prefix: '/rpc',
  version: 'v1',

  rpcPlugins: [
    loggerPlugin(),
    // cachePlugin({ ttl: 5000 })
  ],

  createContext: (ctx) => ({
    headers: ctx.req?.headers ?? {},
  }),

  openapi: true,
  logger: true,

  beforeRequest: (ctx) => {
    ctx.start = Date.now()
  },

  afterResponse: (ctx, result) => ({
    success: true,
    data: result,
  }),

  onError: (err, ctx) => {
    return ctx.statusCode(400).json({
      success: false,
      message: err.message,
    })
  },

  dev: true,
})

Built-in Routes

POST /rpc
POST /rpc/v1

GET /openapi.json
GET /health

Hooks

beforeRequest(ctx)
afterResponse(ctx, result)
onError(err, ctx)

Logger

[RPC] POST /rpc/v1 - 12ms
[RPC ERROR] POST /rpc/v1 - 5ms

Unified Handler (Node / Edge / Lambda)

One handler for all runtimes


Usage

import { createHandler } from 'railiz-rpc/adapter'

export const handler = createHandler({
  router: rpc,
  runtime: 'lambda', // 'node' | 'edge' | 'lambda'
})

Real Example (RPC)

import { createRPC, createRouter, createProcedure } from 'railiz-rpc'
import { createHandler } from 'railiz-rpc/adapter'

// ====================
// Procedure
// ====================
const t = createProcedure()

// ====================
// Router
// ====================
const router = createRouter({
  hello: t.resolve(() => {
    return 'hello world'
  }),
})

// ====================
// RPC
// ====================
const rpc = createRPC({
  app: router,
})

// ====================
// Lambda handler
// ====================
export const handler = createHandler({
  router: rpc,
  runtime: 'lambda',
})

Call

POST /rpc

{
  "path": "app.hello",
  "input": {}
}

Response

{
  "success": true,
  "data": "hello world"
}

Supported Runtimes

| Runtime | Usage | | ------- | ----------------------------- | | Node | Express / Fastify | | Edge | Cloudflare / Bun / Workers | | Lambda | AWS Lambda / Vercel / Netlify |


What you get

  • ✅ One API → all runtimes
  • ✅ Auto normalize headers
  • ✅ Base64 body support (AWS)
  • ✅ Multi-value headers support
  • ✅ Query string support
  • ✅ Built-in CORS fallback
  • ✅ Zero core changes

Runtime → Request → RPC → Response → Runtime


Result

createHandler({ router, runtime: 'lambda' })

→ deploy anywhere


Bridge Layer (Core Concept)

Server Side (Source of Truth)

const rpc = createRPC({
  user: userRouter,
  post: postRouter,
})

// export
export type AppRouter = typeof rpc

Then you mount it into any runtime (Railiz / Node / Edge / Lambda):

app.plugin(
  rpcHandler(rpc, {
    prefix: '/rpc',
  }),
)

At this point, your entire backend is exposed through a single endpoint:

POST /rpc

Client Side (Type-Safe Proxy)

On the client, you do NOT recreate APIs or endpoints. You simply import the type from the server and call it like a local function:

// ts
import type { AppRouter } from 'yourbackend'

const rpc = rpcClient<AppRouter>('/rpc')

await rpc.user.get({ id: 1 })
await rpc.post.list()

No REST. No DTO duplication. No manual API layer.


Bridge Flow

Client Call
   ↓
Type-safe Proxy
   ↓
HTTP Transport (/rpc)
   ↓
RPC Handler (Railiz / Node / Edge)
   ↓
Router → Procedure → Middleware
   ↓
Response

Mental Model

Think of Railiz-rpc as:

  • Server = function registry
  • Client = typed function proxy
  • Transport = invisible bridge
  • RPC = shared contract layer

You are no longer building APIs. You are sharing functions across runtime boundaries.


Key Idea

  • Write functions once on the server
  • Call them anywhere on the client
  • Let Railiz-rpc handle everything in between This is the core philosophy of Railiz-rpc: “Functions over APIs.”

tRPC vs railiz-rpc

| ⚙️ Criteria | 🧩 tRPC | 🚀 railiz-rpc | | ---------------- | ------------------- | -------------------------- | | Runtime | Node only | Node + Edge + Serverless | | Architecture | Fixed / opinionated | Flexible / composable | | Router system | Centralized | Multi-router (modular) | | Plugins | Limited | Fully extensible | | Middleware | Basic | First-class + composable | | Transport layer | HTTP-centric | Pluggable (fetch, ws, etc) | | Client DX | Excellent | Excellent (tRPC-like) | | Setup complexity | Medium | Minimal | | Core size | Heavy | Lightweight | | OpenAPI | External / partial | Built-in | | Edge support | Limited | First-class | | Serverless | Workarounds needed | Native support |


Why this matters

  • Call backend like local functions
  • Share types automatically
  • Works across runtimes (Node, Edge, Lambda)
  • No API layer duplication

Philosophy

RPC should feel like:

rpc.user.get({ id: 1 })

Not HTTP. Not REST.


License

MIT