Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ We use **Vitest** for testing and **ESLint** with [@antfu/eslint-config](https:/
5. **Code**: Make your changes.
6. **Test**: Manually verify in a playground, e.g.:
```bash
cd playgrounds/nextjs
cd playgrounds/next
pnpm dev
```
7. **Tests**: Add or update tests:
Expand Down
2 changes: 1 addition & 1 deletion apps/content/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default defineConfig({
items: [
{ text: 'Express', link: '/docs/integrations/express' },
{ text: 'Fastify', link: '/docs/integrations/fastify' },
{ text: 'Next.js', link: '/docs/integrations/nextjs' },
{ text: 'Next.js', link: '/docs/integrations/next' },
{ text: 'Nuxt', link: '/docs/integrations/nuxt' },
{ text: 'Hono', link: '/docs/integrations/hono' },
{ text: 'Tanstack Start', link: '/docs/integrations/tanstack-start' },
Expand Down
190 changes: 190 additions & 0 deletions apps/content/docs/integrations/next.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
title: Next.js Integration
description: Seamlessly integrate oRPC with Next.js
---

# Next.js Integration

[Next.js](https://nextjs.org/) is a leading React framework for server-rendered apps. oRPC works with both the [App Router](https://nextjs.org/docs/app/getting-started/installation) and [Pages Router](https://nextjs.org/docs/pages/getting-started/installation). For additional context, refer to the [HTTP Adapter](/docs/adapters/http) guide.

::: info
oRPC also provides out-of-the-box support for [Server Action](/docs/server-action) with no additional configuration required.
:::

## Server

You can integrate oRPC with TanStack Start using its [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).

::: code-group

```ts [app/rpc/[[...rest]]/route.ts]
import { RPCHandler } from '@orpc/server/fetch'

const handler = new RPCHandler(router)

async function handleRequest(request: Request) {
const { response } = await handler.handle(request, {
prefix: '/rpc',
context: {}, // Provide initial context if needed
})

return response ?? new Response('Not found', { status: 404 })
}

export const HEAD = handleRequest
export const GET = handleRequest
export const POST = handleRequest
export const PUT = handleRequest
export const PATCH = handleRequest
export const DELETE = handleRequest
```

:::

::: info
The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc-handler), [OpenAPIHandler](/docs/openapi/openapi-handler), or another custom handler.
:::

::: details Pages Router Support?

```ts [pages/rpc/[[...rest]].ts]
import { RPCHandler } from '@orpc/server/node'

const handler = new RPCHandler(router)

export const config = {
api: {
bodyParser: false,
},
}

export default async (req, res) => {
const { matched } = await handler.handle(req, res, {
prefix: '/rpc',
context: {}, // Provide initial context if needed
})

if (matched) {
return
}

res.statusCode = 404
res.end('Not found')
}
```

::: warning

Next.js default [body parser](https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config) blocks oRPC raw‑request handling. Ensure `bodyParser` is disabled in your API route:

```ts
export const config = {
api: {
bodyParser: false,
},
}
```

:::

## Client

Next.js doesn’t natively support isomorphic functions, so you need a workaround to make client-side code compatible with SSR. This example uses `globalThis.$headers` as that workaround. Alternatively, you can use React Context like the approach mentioned in [discussions#330](https://github.com/unnoq/orpc/discussions/330#discussioncomment-12727779).

::: code-group

```ts [lib/orpc.ts]
import type { headers } from 'next/headers'

declare global {
var $headers: typeof headers
}

const link = new RPCLink({
url: new URL('/rpc', typeof window !== 'undefined' ? window.location.href : 'http://localhost:3000'),
headers: async () => {
return globalThis.$headers
? Object.fromEntries(await globalThis.$headers()) // use this on ssr
: {} // use this on browser
},
})
```

```ts [lib/orpc.server.ts]
'server only'

import { headers } from 'next/headers'

globalThis.$headers = headers
```

```ts [app/layout.tsx]
import '../lib/orpc.server'

// Rest of the code
```

:::

:::info
This only shows how to configure the link. For full client examples, see [Client-Side Clients](/docs/client/client-side).
:::

## Optimize SSR

To reduce HTTP requests and improve latency during SSR, you can utilize a [Server-Side Client](/docs/client/server-side) during SSR. Below is a quick setup, see [Optimize SSR](/docs/best-practices/optimize-ssr) for a more details.

::: code-group

```ts [lib/orpc.ts]
import type { RouterClient } from '@orpc/server'
import { RPCLink } from '@orpc/client/fetch'
import { createORPCClient } from '@orpc/client'

declare global {
var $client: RouterClient<typeof router> | undefined
}

const link = new RPCLink({
url: () => {
if (typeof window === 'undefined') {
throw new Error('RPCLink is not allowed on the server side.')
}

return new URL('/rpc', window.location.href)
},
})

/**
* Fallback to client-side client if server-side client is not available.
*/
export const client: RouterClient<typeof router> = globalThis.$client ?? createORPCClient(link)
```

```ts [lib/orpc.server.ts]
'server only'

import { headers } from 'next/headers'
import { createRouterClient } from '@orpc/server'

globalThis.$client = createRouterClient(router, {
/**
* Provide initial context if needed.
*
* Because this client instance is shared across all requests,
* only include context that's safe to reuse globally.
* For per-request context, use middleware context or pass a function as the initial context.
*/
context: async () => ({
headers: await headers(),
}),
})
```

```ts [app/layout.tsx]
import '../lib/orpc.server'

// Rest of the code
```

:::
92 changes: 0 additions & 92 deletions apps/content/docs/integrations/nextjs.md

This file was deleted.

4 changes: 2 additions & 2 deletions apps/content/docs/playgrounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ featuring pre-configured examples accessible instantly via StackBlitz or local s

| Environment | StackBlitz | GitHub Source |
| ------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Next.js Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/nextjs) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/nextjs) |
| Next.js Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/next) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/next) |
| TanStack Start Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/tanstack-start) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/tanstack-start) |
| Nuxt.js Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/nuxt) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/nuxt) |
| Solid Start Playground | [Open in StackBlitz](https://stackblitz.com/github/unnoq/orpc/tree/main/playgrounds/solid-start) | [View Source](https://github.com/unnoq/orpc/tree/main/playgrounds/solid-start) |
Expand All @@ -29,7 +29,7 @@ StackBlitz has own limitations, so some features may not work as expected.
If you prefer working locally, you can clone any playground using the following commands:

```bash
npx degit unnoq/orpc/playgrounds/nextjs orpc-nextjs-playground
npx degit unnoq/orpc/playgrounds/next orpc-next-playground
npx degit unnoq/orpc/playgrounds/tanstack-start orpc-tanstack-start-playground
npx degit unnoq/orpc/playgrounds/nuxt orpc-nuxt-playground
npx degit unnoq/orpc/playgrounds/solid-start orpc-solid-start-playground
Expand Down
3 changes: 2 additions & 1 deletion apps/content/public/_redirects
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/docs /docs/getting-started 301
/docs/openapi /docs/openapi/getting-started 301
/sponsor https://github.com/sponsors/unnoq 301
/sponsor https://github.com/sponsors/unnoq 301
/docs/integrations/nextjs /docs/integrations/next 301
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@orpc/nextjs-playground",
"name": "@orpc/next-playground",
"version": "1.2.0",
"private": true,
"scripts": {
Expand Down
File renamed without changes
File renamed without changes
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ async function handleRequest(request: Request) {
return response ?? new Response('Not found', { status: 404 })
}

export const HEAD = handleRequest
export const GET = handleRequest
export const POST = handleRequest
export const PUT = handleRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../lib/orpc.server'

import type { Metadata } from 'next'
import { Providers } from './providers'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
'use client'

import { orpc } from '@/lib/orpc'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'

export function ListPlanetsQuery() {
const { data, refetch, fetchNextPage, hasNextPage, status } = useInfiniteQuery(
const { data, refetch, fetchNextPage, hasNextPage, status } = useSuspenseInfiniteQuery(
orpc.planet.list.infiniteOptions({
input: cursor => ({ cursor, limit: 10 }),
getNextPageParam: lastPage => lastPage.length === 10 ? lastPage.at(-1)?.id : null,
initialPageParam: 0,
}),
)

if (status === 'pending') {
return <p>Loading...</p>
}

if (status === 'error') {
return (
<p>
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async function handleRequest(request: Request) {
return response ?? new Response('Not found', { status: 404 })
}

export const HEAD = handleRequest
export const GET = handleRequest
export const POST = handleRequest
export const PUT = handleRequest
Expand Down
Loading