Table of Contents
How to use oRPC with SvelteKit
A basic oRPC example with sveltekit
What is oRPC?
Easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards
Setup
Install dependencies
pnpm add @orpc/client @orpc/server zod -D
Create a base type for your router
Creates a base type for your router. This will be used for all procedures so that they have access to the entire sveltekit event in the context.
src/lib/server/orpc/base.ts
import { os } from '@orpc/server'
import type { RequestEvent } from '@sveltejs/kit'
export type BaseContext = { event: RequestEvent }
export const base = os.$context<BaseContext>().errors({
NOT_AUTHORIZED: {
message: "Not authorized",
},
// Define your custom errors here
})
Create router
src/lib/server/orpc/router.ts
import { base } from './base'
export const router = {
hello: base
.handler(async () => {
return "Hello World"
})
}
Create server side client
Type the client
src/app.d.ts
import type { router } from "$lib/server/orpc/router"
import type { RouterClient } from "@orpc/server"
declare global {
namespace App {
interface Locals {
orpc: RouterClient<typeof router>
}
}
}
export { };
Create client
src/hooks.server.ts
import { router } from "$lib/server/orpc/router"
import { createRouterClient } from "@orpc/server"
import type { Handle } from "@sveltejs/kit"
export const handle: Handle = async ({ event, resolve }) => {
event.locals.orpc = createRouterClient(router, {
context: {
event
},
})
const response = await resolve(event)
return response
}
Use the client
src/routes/+page.server.ts
export const load = (async ({ locals }) => {
const hello = await locals.orpc.hello()
return {
hello
};
})
Create client-side client
Create RPC endpoint
src/routes/rpc/[...rest]/+server.ts
import type { RequestHandler } from './$types';
import { RPCHandler } from '@orpc/server/fetch'
import { router } from '$lib/server/orpc/router'
const handler = new RPCHandler(router)
const handle: RequestHandler = async (event) => {
const { response } = await handler.handle(event.request, {
prefix: '/rpc',
context: {
event
},
})
return response ?? new Response('Not Found', { status: 404 })
}
export const GET = handle
export const POST = handle
export const PUT = handle
export const PATCH = handle
export const DELETE = handle
Create client
src/lib/orpc/client.ts
import type { RouterClient } from '@orpc/server'
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import type { router } from '$lib/server/orpc/router'
import { browser } from '$app/environment'
const link = new RPCLink({
url: () => {
if (!browser) {
throw new Error('RPCLink is not allowed on the server side. Use the optimized server-side client instead.')
}
return `${window.location.origin}/rpc`
},
})
export const client: RouterClient<typeof router> = createORPCClient(link)
Use the client
src/routes/+page.svelte
<script lang="ts">
import { browser } from '$app/environment';
import { client } from '$lib/orpc/client';
</script>
{#await client.hello()}
<p>Loading...</p>
{:then value}
<p>{value}</p>
{:catch error}
<p>{error}</p>
{/await}
Middleware
Create middleware
src/lib/server/orpc/middleware/auth.ts
import { base } from "../base"
export const auth = base
.middleware(async ({ context, next, errors }) => {
// throw errors.NOT_AUTHORIZED() if user doesn't exist (do cookie and session check here)
const result = await next({
context: {
...context, // Keep the existing context (including event)
user: {
id: '1',
}
}
})
return result
})
Use middleware
src/lib/server/orpc/router.ts
import { base } from './base'
import { auth } from './middleware/auth'
export const router = {
hello: base
.use(auth)
.handler(async ({context}) => {
// now has access to the user with context.user
return "Hello World"
})
}
OpenAPI
oRPC also has the option to expose your router as an OpenAPI spec. This can be useful for generating client libraries or for documentation.
Create OpenAPI endpoint
src/routes/api/[...rest]/+server.ts
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import type { RequestHandler } from '@sveltejs/kit'
import { ZodSmartCoercionPlugin, ZodToJsonSchemaConverter } from '@orpc/zod'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { router } from '$lib/server/orpc/router'
const handler = new OpenAPIHandler(router, {
// Exclude internal routes from OpenAPI spec (make sure item only has one tag, not sure why)
// filter: ({ contract, path }) => !contract['~orpc'].route.tags?.includes('internal'),
plugins: [
new ZodSmartCoercionPlugin(),
new OpenAPIReferencePlugin({
schemaConverters: [
new ZodToJsonSchemaConverter(),
],
specGenerateOptions: {
info: {
title: 'API',
version: '1.0.0',
},
security: [{ bearerAuth: [] }],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
},
},
// exclude: (procedure, path) => !!procedure['~orpc'].route.tags?.includes('admin'),
},
docsConfig: {
authentication: {
securitySchemes: {
bearerAuth: {
token: 'default-token',
},
},
},
},
}),
],
})
const handle: RequestHandler = async (event) => {
const { response } = await handler.handle(event.request, {
prefix: '/api',
context: {
event
}
})
return response ?? new Response('Not Found', { status: 404 })
}
export const GET = handle
export const POST = handle
export const PUT = handle
export const PATCH = handle
export const DELETE = handle
Now you can go to http://localhost:5173/api
to see the OpenAPI spec.
More info on how to use Modify each endpoint can be found here.
Tanstack Query
Install dependencies
pnpm add @orpc/tanstack-query @tanstack/svelte-query -D
Create tanstack query client
src/lib/orpc/client.ts
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
export const client = // Use client from above
export const tanstackClient = createTanstackQueryUtils(client)
Use the client
src/routes/+page.svelte
<script lang="ts">
import { tanstackClient } from '$lib/orpc/client';
import { createQuery } from '@tanstack/svelte-query'
const query = createQuery(tanstackClient.hello());
</script>
<pre>{JSON.stringify($query.data, null, 2)}</pre>
Permission middleware
If you want to add a middleware for permissions Permix is a great option.
pnpm add permix -D
Create middleware
A simple middleware with two different types of defining permissions.
post
can be checked with data within the middleware
postWithData
can be checked with data that gets passed in to the check function (see usage in next section).
dataType can also be marked as required like this inside createPermix
src/lib/server/orpc/middleware/permix.ts
import { createPermix } from 'permix/orpc'
import { base } from '../base'
export const postPermix = createPermix<{
post: {
action: 'create' | 'delete' | 'view'
},
postWithData: {
dataType: { public: boolean }
action: 'create' | 'delete' | 'view'
}
}>()
export const postGate = base.middleware(async ({ next, context }) => {
const user = { role: 'ADMIN' } // get user from context
const p = postPermix.setup({
post: {
create: true,
view: true,
delete: user.role === 'ADMIN'
},
postWithData: {
create: true,
view: post => post?.public || user.role === 'ADMIN',
delete: user.role === 'ADMIN'
}
})
return next({
context: {
permix: p
}
})
})
Use middleware
You can use the middleware in your router like this:
postWithData
fill return false if it gets called without a post object
src/lib/server/orpc/router.ts
import { base } from './base'
import { postGate, postPermix } from './middleware/permix'
export const router = {
post: base
.use(postGate)
.use(postPermix.checkMiddleware("post", "view"))
.handler(async () => {
// do something
}),
postWithData: base
.use(postGate)
.handler(async ({ errors }) => {
const post = { public: false } // get post from db
if (!postPermix.checkMiddleware("postWithData", "view", post)) throw errors.NOT_AUTHORIZED()
// do something
})
}