Skip to content

Commit

Permalink
feat(server)!: fetch handler rewrite and tests with multiple coercers…
Browse files Browse the repository at this point in the history
… support (#57)

* wip

* CompositeHandler should not condition

* tests

* tests

* tests

* ORPC_PROTOCOL_HEADER -> ORPC_HANDLER_HEADER

* wip

* openapi matcher

* openapi handlers

* fixed

* coercer

* fixed

* remove @orpc/transformer

* docs & examples

* fix types

* docs fixed

* fix & tests zod coercer

* coercers for form actions

* fix zod coercer

* nuxt playground link to scalar

* fix openapi codec on NON GET method

* playgrounds prefer workspace packages

* improve

* applied lint fixes
  • Loading branch information
unnoq authored Dec 27, 2024
1 parent 0640aed commit 93e7a4c
Show file tree
Hide file tree
Showing 110 changed files with 8,739 additions and 4,892 deletions.
4 changes: 3 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use-node-version = 22.11.0
auto-install-peers = true
shamefully-hoist = true
shamefully-hoist = true
link-workspace-packages = true
prefer-workspace-packages = true
21 changes: 12 additions & 9 deletions apps/content/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,29 @@ In oRPC middleware is very useful and fully typed you can find more info [here](
This example uses [@whatwg-node/server](https://www.npmjs.com/package/@whatwg-node/server) to create a Node server with [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
```ts twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { createServer } from 'node:http'
import { createServerAdapter } from '@whatwg-node/server'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

const server = createServer(
createServerAdapter((request: Request) => {
const url = new URL(request.url)

if (url.pathname.startsWith('/api')) {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
prefix: '/api',
context: {},
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
}

Expand Down
28 changes: 10 additions & 18 deletions apps/content/content/docs/server/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,14 @@ export const router = base.router({
// You can call this procedure directly without manually providing context
const output = await router.getting()

import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch'

const orpcHandler = new ORPCHandler(router)

export function fetch(request: Request) {
// No need to pass context; middleware handles it
return handleFetchRequest({
router,
request,
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler()
],
})
return orpcHandler.fetch(request)
}
```

Expand All @@ -106,8 +101,8 @@ rather than relying on global mechanisms like `headers` or `cookies` in Next.js.

```ts twoslash
import { os, ORPCError, createProcedureClient } from '@orpc/server'
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch'

type ORPCContext = { user?: { id: string }, db: 'fake-db' }

Expand All @@ -133,17 +128,14 @@ export const router = base.router({
}),
})

const orpcHandler = new ORPCHandler(router)

export function fetch(request: Request) {
// Initialize context explicitly for each request
const db = 'fake-db' as const
const user = request.headers.get('Authorization') ? { id: 'example' } : undefined

return handleFetchRequest({
router,
request,
context: { db, user },
handlers: [createORPCHandler(), createOpenAPIServerlessHandler()],
})
return orpcHandler.fetch(request, { context: { db, user } })
}

// If you want to call this procedure or use as server action
Expand Down
132 changes: 74 additions & 58 deletions apps/content/content/docs/server/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,22 @@ Whether you're targeting serverless, edge environments, or traditional backends,
## Quick Example

```ts twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

export function fetch(request: Request) {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
context: {},
// prefix: '/api', // Optionally define a route prefix
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
}
```
Expand All @@ -37,23 +39,25 @@ Node.js doesn't provide native support for creating server with [Fetch API](http
but you can easily use [@whatwg-node/server](https://npmjs.com/package/@whatwg-node/server) as an adapter.

```ts twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { createServer } from 'node:http'
import { createServerAdapter } from '@whatwg-node/server'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

const server = createServer(
createServerAdapter((request: Request) => {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
context: {},
// prefix: '/api',
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
})
)
Expand All @@ -66,24 +70,27 @@ server.listen(3000, () => {
## Express.js

```ts twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { createServerAdapter } from '@whatwg-node/server'
import express from 'express'
import { createServerAdapter } from '@whatwg-node/server'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

const app = express()

app.all('/api/*', createServerAdapter((request: Request) => {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
context: {},
prefix: '/api',
handlers: [
createORPCHandler(),
createOpenAPIServerHandler(),
],
})
}))

Expand All @@ -96,23 +103,26 @@ app.listen(3000, () => {

```ts twoslash
import { Hono } from 'hono'
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

const app = new Hono()

app.get('/api/*', (c) => {
return handleFetchRequest({
router,
request: c.req.raw,
return compositeHandler.fetch(c.req.raw, {
prefix: '/api',
context: {},
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
})
})

export default app
Expand All @@ -121,20 +131,23 @@ export default app
## Next.js

```ts title="app/api/[...orpc]/route.ts" twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

export function GET(request: Request) {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
prefix: '/api',
context: {},
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
}

Expand All @@ -147,21 +160,24 @@ export const PATCH = GET
## Cloudflare Workers

```ts twoslash
import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch'
import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch'
import { ORPCHandler, CompositeHandler } from '@orpc/server/fetch'
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { router } from 'examples/server'
import { ZodCoercer } from '@orpc/zod'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

export default {
async fetch(request: Request) {
return handleFetchRequest({
router,
request,
return compositeHandler.fetch(request, {
prefix: '/',
context: {},
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
})
},
}
Expand Down
3 changes: 3 additions & 0 deletions apps/content/content/docs/server/server-action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ React forms integrate seamlessly with server actions using `createFormAction`. T
// on server
import { createFormAction } from '@orpc/next'
import { updateUser } from 'examples/server-action'
import { ZodCoercer } from '@orpc/zod'

const updateUserFA = createFormAction({
procedure: updateUser,
schemaCoercers: [new ZodCoercer()],
onSuccess: () => {
// redirect('/some-where')
}
Expand Down
1 change: 1 addition & 0 deletions apps/content/content/home/landing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const { execute, isPending, isError, error, output, input, status } = useAction(
// use as a form action
const gettingFA = createFormAction({
procedure: getting,
schemaCoercers: [new ZodCoercer()],
onSuccess: () => redirect('/some-where')
})

Expand Down
33 changes: 17 additions & 16 deletions apps/content/examples/contract.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { InferContractRouterInputs, InferContractRouterOutputs } from '@orpc/contract'
import { oc } from '@orpc/contract'
import { oz } from '@orpc/zod'
import { z } from 'zod'

// Implement the contract

import { ORPCError, os } from '@orpc/server'
import { oz, ZodCoercer } from '@orpc/zod'
import { z } from 'zod'

// Define your contract first
// This contract can replace server router in most-case
Expand Down Expand Up @@ -63,6 +60,8 @@ export const contract = oc.router({
export type Inputs = InferContractRouterInputs<typeof contract>
export type Outputs = InferContractRouterOutputs<typeof contract>

// Implement the contract

export type Context = { user?: { id: string } }
export const base = os.context<Context>()
export const pub /** os with ... */ = base.contract(contract) // Ensure every implement must be match contract
Expand Down Expand Up @@ -114,27 +113,29 @@ export const router = pub.router({
},
})

// Expose apis to the internet with fetch handler
import { createOpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch'
// Modern runtime that support fetch api like deno, bun, cloudflare workers, even node can used
import { createServer } from 'node:http'
// Expose apis to the internet with fetch handler
import { OpenAPIServerlessHandler } from '@orpc/openapi/fetch'
import { CompositeHandler, ORPCHandler } from '@orpc/server/fetch'
import { createServerAdapter } from '@whatwg-node/server'

const openapiHandler = new OpenAPIServerlessHandler(router, {
schemaCoercers: [
new ZodCoercer(),
],
})
const orpcHandler = new ORPCHandler(router)
const compositeHandler = new CompositeHandler([openapiHandler, orpcHandler])

const server = createServer(
createServerAdapter((request: Request) => {
const url = new URL(request.url)

if (url.pathname.startsWith('/api')) {
return handleFetchRequest({
router,
request,
prefix: '/api',
return compositeHandler.fetch(request, {
context: {},
handlers: [
createORPCHandler(),
createOpenAPIServerlessHandler(),
],
prefix: '/api',
})
}

Expand Down
Loading

0 comments on commit 93e7a4c

Please sign in to comment.