Integrate tRPC server to Bun with Elysia
tRPC has been a popular choice for web development recently, thanks to its end-to-end type-safety approach to accelerate development by blurring the line between front and backend, and inferring types from the backend automatically.
Helping developers develop faster and safer code, knowing instantly when things break while migrating data structure, and removing redundant steps of re-creating type for frontend once again.
But we can when extending tRPC more.
Elysia
Elysia is a web framework optimized for Bun, inspired by many frameworks including tRPC. Elysia supports end-to-end type safety by default, but unlike tRPC, Elysia uses Express-like syntax that many already know, removing the learning curve of tRPC.
With Bun being the runtime for Elysia, the speed and throughput for Elysia server are fast and even outperforming Express up to 21x and Fastify up to 12x on mirroring JSON body (see benchmark).
The ability to combine the existing tRPC server into Elysia has been one of the very first objectives of Elysia since its start.
The reason why you might want to switch from tRPC to Bun:
- Significantly faster, even outperform many popular web frameworks running in Nodejs without changing a single piece of code.
- Extend tRPC with RESTful or GraphQL, both co-existing in the same server.
- Elysia has end-to-end type-safety like tRPC but with almost no-learning curve for most developer.
- Using Elysia is the great first start experimenting/investing in Bun runtime.
Creating Elysia Server
To get started, let's create a new Elysia server, make sure you have Bun installed first, then run this command to scaffold Elysia project.
bun create elysia elysia-trpc && cd elysia-trpc && bun add elysia
TIP
Sometimes Bun doesn't resolve the latest field correctly, so we are using bun add elysia
to specify the latest version of Elysia instead
This should create a folder name "elysia-trpc" with Elysia pre-configured.
Let's start a development server by running the dev command:
bun run dev
This command should start a development server on port :3000
Elysia tRPC plugin
Building on top of the tRPC Web Standard adapter, Elysia has a plugin for integrating the existing tRPC server into Elysia.
bun add @trpc/server zod @elysiajs/trpc @elysiajs/cors
Suppose that this is an existing tRPC server:
import { initTRPC } from '@trpc/server'
import { observable } from '@trpc/server/observable'
import { z } from 'zod'
const t = initTRPC.create()
export const router = t.router({
mirror: t.procedure.input(z.string()).query(({ input }) => input),
})
export type Router = typeof router
Normally all we need to use tRPC is to export the type of router, but to integrate tRPC with Elysia, we need to export the instance of router too.
Then in the Elysia server, we import the router and register tRPC router with .use(trpc)
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { trpc } from '@elysiajs/trpc'
import { router } from './trpc'
const app = new Elysia()
.use(cors())
.get('/', () => 'Hello Elysia')
.use(
trpc(router)
)
.listen(3000)
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
And that's it! 🎉
That's all it takes to integrate tRPC with Elysia, making tRPC run on Bun.
tRPC config and Context
To create context, trpc
can accept 2nd parameters that can configure tRPC as same as createHTTPServer
.
For example, adding createContext
into tRPC server:
// trpc.ts
import { initTRPC } from '@trpc/server'
import { observable } from '@trpc/server/observable'
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
import { z } from 'zod'
export const createContext = async (opts: FetchCreateContextFnOptions) => {
return {
name: 'elysia'
}
}
const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create()
export const router = t.router({
mirror: t.procedure.input(z.string()).query(({ input }) => input),
})
export type Router = typeof router
And in the Elysia server
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import '@elysiajs/trpc'
import { router, createContext } from './trpc'
const app = new Elysia()
.use(cors())
.get('/', () => 'Hello Elysia')
.use(
trpc(router, {
createContext
})
)
.listen(3000)
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
And we can specify a custom endpoint of tRPC by using endpoint
:
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { trpc } from '@elysiajs/trpc'
import { router, createContext } from './trpc'
const app = new Elysia()
.use(cors())
.get('/', () => 'Hello Elysia')
.use(
trpc(router, {
createContext,
endpoint: '/v2/trpc'
})
)
.listen(3000)
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
Subscription
By default, tRPC uses WebSocketServer to support subscription
, but unfortunately as Bun 0.5.4 doesn't support WebSocketServer yet, we can't directly use WebSocket Server.
However, Bun does support Web Socket using Bun.serve
, and with Elysia tRPC plugin has wired all the usage of tRPC's Web Socket into Bun.serve
, you can directly use tRPC's subscription
with Elysia Web Socket plugin directly:
Start by installing the Web Socket plugin:
bun add @elysiajs/websocket
Then inside tRPC server:
import { initTRPC } from '@trpc/server'
import { observable } from '@trpc/server/observable'
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
import { EventEmitter } from 'stream'
import { zod } from 'zod'
export const createContext = async (opts: FetchCreateContextFnOptions) => {
return {
name: 'elysia'
}
}
const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create()
const ee = new EventEmitter()
export const router = t.router({
mirror: t.procedure.input(z.string()).query(({ input }) => {
ee.emit('listen', input)
return input
}),
listen: t.procedure.subscription(() =>
observable<string>((emit) => {
ee.on('listen', (input) => {
emit.next(input)
})
})
)
})
export type Router = typeof router
And then we register:
import { Elysia, ws } from 'elysia'
import { cors } from '@elysiajs/cors'
import '@elysiajs/trpc'
import { router, createContext } from './trpc'
const app = new Elysia()
.use(cors())
.use(ws())
.get('/', () => 'Hello Elysia')
.trpc(router, {
createContext
})
.listen(3000)
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
And that's all it takes to integrate the existing fully functional tRPC server to Elysia Server thus making tRPC run on Bun 🥳.
Elysia is excellent when you need both tRPC and REST API, as they can co-exist together in one server.
Bonus: Type-Safe Elysia with Eden
As Elysia is inspired by tRPC, Elysia also supports end-to-end type-safety like tRPC by default using "Eden".
This means that you can use Express-like syntax to create RESTful API with full-type support on a client like tRPC.
To get started, let's export the app type.
import { Elysia, ws } from 'elysia'
import { cors } from '@elysiajs/cors'
import { trpc } from '@elysiajs/trpc'
import { router, createContext } from './trpc'
const app = new Elysia()
.use(cors())
.use(ws())
.get('/', () => 'Hello Elysia')
.use(
trpc(router, {
createContext
})
)
.listen(3000)
export type App = typeof app
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
And on the client side:
bun add @elysia/eden && bun add -d elysia
And in the code:
import { edenTreaty } from '@elysiajs/eden'
import type { App } from '../server'
// This now has all type inference from the server
const app = edenTreaty<App>('http://localhost:3000')
// data will have a value of 'Hello Elysia' and has a type of 'string'
const data = await app.index.get()
Elysia is a good start when you want end-to-end type-safety like tRPC but need to support more standard patterns like REST, and still have to support tRPC or need to migrate from one.
Bonus: Extra tip for Elysia
An additional thing you can do with Elysia is not only that it has support for tRPC and end-to-end type-safety, but also has a variety of support for many essential plugins configured especially for Bun.
For example, you can generate documentation with Swagger only in 1 line using Swagger plugin.
import { Elysia, t } from 'elysia'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()
.use(swagger())
.setModel({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/', () => 'Hello Elysia')
.post('/typed-body', ({ body }) => body, {
schema: {
body: 'sign',
response: 'sign'
}
})
.listen(3000)
export type App = typeof app
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
Or when you want to use GraphQL Apollo on Bun.
import { Elysia } from 'elysia'
import { apollo, gql } from '@elysiajs/apollo'
const app = new Elysia()
.use(
apollo({
typeDefs: gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`,
resolvers: {
Query: {
books: () => {
return [
{
title: 'Elysia',
author: 'saltyAom'
}
]
}
}
}
})
)
.get('/', () => 'Hello Elysia')
.listen(3000)
export type App = typeof app
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
Or supporting OAuth 2.0 with a community OAuth plugin.
Nonetheless, Elysia is a great place to start learning/using Bun and the ecosystem around Bun.
If you like to learn more about Elysia, Elysia documentation is a great start to start exploring the concept and patterns, and if you are stuck or need help, feel free to reach out in Elysia Discord.
The repository for all of the code is available at https://github.com/saltyaom/elysia-trpc-demo, feels free to experiment and reach out.