## 🌐 railiz-rpc

[![NPM](https://img.shields.io/npm/v/railiz-rpc.svg)](https://www.npmjs.com/package/railiz-rpc) ![Downloads](https://img.shields.io/npm/dt/railiz-rpc.svg) ![Bundle Size](https://img.shields.io/bundlephobia/minzip/railiz-rpc)

[LIVE EXAMPLE](https://codesandbox.io/p/devbox/8wpd2w)

🚀 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

```ts
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

```bash
npm install railiz-rpc railiz zod
```

---

## Mental Model

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

Everything is:

* fully typed
* composable
* framework-agnostic

---

## Quick Start

```ts
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)

```ts
// 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:

```ts
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:

```ts
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

```ts

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

```ts
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)

```ts
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

```ts
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)

```ts
await client.user.get({ id: 1 })
```

```
rpcReactClient -> Proxy (user.get) -> useQuery / useMutation / direct call -> fetch transport -> railiz-rpc server
```

---

## Multi Router

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

---

## Middleware

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

---

## OpenAPI

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

const spec = openApi(rpc)
```

---

## Multi RPC Instances

```ts
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**

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

**Batch call**

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

### Response

All responses are always wrapped in a standard envelope:

```json
{
  "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:

```json
{
  "success": true,
  "data": {
    "error": "RPC_ERROR"
  }
}
```

or in batch:
```json
{
  "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)

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

export default {
  fetch: createFetchHandler(rpc),
}
```

---

### Express

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

const app = express()

app.use(express.json())

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

---

### Fastify

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

const app = Fastify()

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

---

### Hono

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

const app = new Hono()

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

---

### Next.js (Route Handler)

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

export const POST = createFetchHandler(rpc)
```

---

### WebSocket

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

const ws = createWebSocketHandler(rpc)
```

---

## createApp (Advanced Server)

> One function to bootstrap everything

### Basic

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

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

### Full Setup

```ts
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

```ts
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

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

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

---

## Real Example (RPC)

```ts
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

```json
POST /rpc

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

---

### Response

```json
{
  "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

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

→ deploy anywhere

---

## Bridge Layer (Core Concept)

### Server Side (Source of Truth)

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

// export
export type AppRouter = typeof rpc
```

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

```ts
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
// 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

```bash
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:

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

Not HTTP. Not REST.

---


## License

MIT
