Skip to content

Makay11/rpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🌐 @makay/rpc

An RPC library for quick development of seamless full-stack applications.

Powered by a Vite plugin and inspired by Telefunc, tRPC and other similar libraries.



✨ Features:

  • 🎉 End-to-end TypeScript
  • 🚫 Zero boilerplate
  • 📡 Optional server-sent events support for real-time subscriptions
  • 🪶 Extremely small client bundle size addition
  • 🔗 Directly import and call tailored server functions from client code
  • 📄 Colocate server and client files (or don't)
  • 📦 Front-end and back-end framework agnostic
  • 📦 Validation library agnostic
  • 🚫 Low server overhead with no implicit run-time validations
  • 🪝 Use the composables/hooks pattern in server code
  • 🔌 Includes adapters for popular libraries like Hono and Zod
  • 🧰 Includes utilities for async server state and results

🔧 Installation and setup

  1. Install a single package:

    npm i @makay/rpc
    yarn add @makay/rpc
    pnpm add @makay/rpc
    bun add @makay/rpc

    Everything is included out-of-the-box!

  2. Set up the Vite plugin:

    // vite.config.ts
    import { rpc } from "@makay/rpc/vite"
    import { defineConfig } from "vite"
    
    export default defineConfig({
      plugins: [rpc()],
    })

    You can run both vite to start a dev server or vite build to build for production.

  3. Set up the RPC server (example using the included Hono adapter):

    // src/server.ts
    import { serve } from "@hono/node-server"
    import { createRpc } from "@makay/rpc/hono"
    import { Hono } from "hono"
    import { cors } from "hono/cors"
    
    const app = new Hono()
    
    app.use(
      "/rpc",
      cors({ origin: "http://localhost:5173", credentials: true }),
      await createRpc()
    )
    
    serve(app, (info) => {
      console.log(`Server is running on http://localhost:${info.port}`)
    })

    You can run the above file with something like npx tsx src/server.ts.

    You can also run npx tsx watch src/server.ts to auto-reload during development.

  4. Configure your client:

    // src/main.ts
    import { config } from "@makay/rpc/client"
    
    config.url = "http://localhost:3000/rpc"
    config.credentials = "include"

🚀 Usage

Create client and server files and seamlessly import server types and functions from client code with full TypeScript support!

// src/components/Todos.ts
import { createTodo, getTodos, type Todo } from "./Todos.server"

let todos: Todo[] = []

async function main() {
  todos = await getTodos()

  console.log(todos)

  const newTodo = await createTodo("New Todo")

  console.log(newTodo)
}

main()
// src/components/Todos.server.ts
export type Todo = {
  id: string
  text: string
}

const todos: Todo[] = []

export async function getTodos() {
  return todos
}

export async function createTodo(text: string) {
  const todo = {
    id: crypto.randomUUID(),
    text,
  }

  todos.push(todo)

  return todo
}

Serve the above src/components/Todos.ts through Vite and you should see the array of todos printed to your browser console. Reload the page a bunch of times and you should see the array grow since the state is persisted in the server!

In a real scenario you would store your data in a database rather than in the server memory, of course. The snippets above are merely illustrative.

📝 Input validation

There is no implicit run-time validation of inputs in the server. In the example above, the function createTodo expects a single string argument. However, if your server is exposed publicly, bad actors or misconfigured clients might send something unexpected which can cause undefined behavior in you program.

Therefore, it is extremely recommended that you validate all function inputs. You can use any validation library you want for this.

Here's a basic example using Zod:

// src/components/Todos.server.ts
import { z } from "zod"

const TextSchema = z.string().min(1).max(256)

export async function createTodo(text: string) {
  TextSchema.parse(text)

  // `text` is now safe to use since Zod would have
  // thrown an error if it was invalid
}

When using the Hono adapter, for instance, the code above will result in a 500 Internal Server Error when you send an invalid input. In order to return the expected 400 Bad Request instead, you have many options depending on the libraries, frameworks, and adapters you are using. Here are a few examples:

  1. Catch the Zod error and throw a ValidationError from @makay/rpc/server instead:

    import { ValidationError } from "@makay/rpc/server"
    
    const TextSchema = z.string().min(1).max(256)
    
    export async function createTodo(text: string) {
      try {
        TextSchema.parse(text)
      } catch (error) {
        if (error instanceof ZodError) {
          throw new ValidationError(error.error.format())
        }
        throw error
      }
    
      // `text` is now safe to use
    }

    This is of course a bit too verbose to be practical.

  2. Use the included Zod adapter:

    import { z, zv } from "@makay/rpc/zod"
    
    const TextSchema = z.string().min(1).max(256)
    
    export async function createTodo(text: string) {
      zv(text, TextSchema)
    
      // `text` is now safe to use
    }

    This is much less verbose than the previous option.

  3. Use the onError callback of the Hono adapter:

    import { serve } from "@hono/node-server"
    import { createRpc } from "@makay/rpc/hono"
    import { Hono } from "hono"
    import { cors } from "hono/cors"
    import { ZodError } from "zod"
    
    const app = new Hono()
    
    const rpc = await createRpc({
      onError(ctx, error) {
        if (error instanceof ZodError) {
          return ctx.json(error.error.format(), 400)
        }
        throw error
      },
    })
    
    app.use(
      "/rpc",
      cors({ origin: "http://localhost:5173", credentials: true }),
      rpc
    )
    
    serve(app, (info) => {
      console.log(`Server is running on http://localhost:${info.port}`)
    })

    This allows you to catch any unhandled Zod errors and return 400 Bad Request regardless of which server function threw the error.

You can easily adapt any of the examples above to work with any libraries and frameworks you are using. Remember that @makay/rpc is completely agnostic.

🚨 Errors

WIP

📦 Async server state

WIP

👍 Results

WIP

📡 Subscriptions

WIP

🔌 Adapters

🔥 Hono

WIP

💎 Zod

WIP

🧑🏻‍💻 Contributing

Contributions, issues, suggestions, ideas and discussions are all welcome!

This is an extremely young library and a lot can still change, be added and removed.

📄 License

MPL-2.0