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

Website

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
        })
}