Compare commits

...

85 Commits

Author SHA1 Message Date
wobsoriano
7c8664d37d chore: release v0.4.4 2022-12-18 23:43:20 -08:00
Robert Soriano
be20e63b79 Merge pull request #54 from wobsoriano/with-client
Bringing back tRPC Client
2022-12-18 23:38:54 -08:00
wobsoriano
5b1cdb6846 update installation 2022-12-18 23:33:47 -08:00
wobsoriano
80573fb32e move links outside of client 2022-12-18 23:31:55 -08:00
wobsoriano
f01b8d5d8d docs: improve docs 2022-12-18 23:25:46 -08:00
wobsoriano
09e0a6c479 feat: option to select headers to pass to useRequestHeaders in http links 2022-12-18 22:35:36 -08:00
wobsoriano
a5cb0c754e move links to another file 2022-12-18 22:22:15 -08:00
wobsoriano
ff88817168 feat: add convenience link wrappers 2022-12-18 22:17:37 -08:00
wobsoriano
f1ca99521e remove unused imports 2022-12-18 21:53:20 -08:00
wobsoriano
143092a16a add engines property 2022-12-18 21:51:03 -08:00
wobsoriano
27f7ed5fb4 eslint fix 2022-12-18 21:50:10 -08:00
wobsoriano
cf38d0a13b chore: update deps 2022-12-18 21:49:45 -08:00
wobsoriano
c14a154723 update example 2022-12-18 21:42:56 -08:00
wobsoriano
0f00e561a1 docs: add simple and recommended usage pages 2022-12-18 21:42:12 -08:00
wobsoriano
0f3c0fff39 docs: bump @nuxt-themes/docus to 1.1.10 2022-12-18 20:13:54 -08:00
wobsoriano
483b6e8076 feat: remove useMutation 2022-12-18 19:43:51 -08:00
wobsoriano
491c04739a cleanup 2022-12-18 16:14:59 -08:00
wobsoriano
b1ddfc146b cleanup 2022-12-18 15:36:11 -08:00
wobsoriano
ee85f3ccd1 feat: add custom Nuxt client 2022-12-18 15:35:56 -08:00
Robert Soriano
d8d4c92ae8 Merge pull request #53 from cawa-93/patch-1
docs: Add missed import in code sample
2022-12-14 23:21:48 -08:00
Alex Kozack
00da42d77e docs: Add missed import in code sample 2022-12-13 18:07:15 +02:00
Robert Soriano
3db1100c87 Merge pull request #51 from cawa-93/patch-1
docs: Replace regular `fetch` with a `$fetch` from nuxt
2022-12-12 20:42:12 -08:00
Robert Soriano
fdde3f1db4 Merge pull request #52 from cawa-93/next
docs: Infer `Context` type
2022-12-12 19:39:41 -08:00
Alex Kozack
4ff57dfb97 docs: Infer Context type 2022-12-12 15:56:53 +02:00
Alex Kozack
aba32c7541 docs: Replace regular fetch with a $fetch from nuxt
This feature was implementet in https://github.com/wobsoriano/trpc-nuxt/pull/28 and describe better way to work with ssr
2022-12-12 15:17:28 +02:00
Robert Soriano
01c2bd5701 Merge pull request #47 from nkhdo/patch-2
Update 1.installation.md
2022-11-28 10:51:57 -08:00
Hoang Do
fe2e45be70 Update 1.installation.md
`@trpc/server` and `@trpc/client` are now pointing to v10, while the `next` tag is deprecated
2022-11-28 17:01:59 +07:00
wobsoriano
2c2df9e2bd chore: release v0.4.3 2022-11-24 13:12:18 -08:00
wobsoriano
5cf1acfa7e feat(deps): update minimum required @trpc/server and @trpc/nuxt version to 10.0.0 2022-11-24 13:12:14 -08:00
wobsoriano
f67f7fc5f4 update docs 2022-11-24 13:10:10 -08:00
wobsoriano
e11edc59eb update docs 2022-11-24 13:08:28 -08:00
wobsoriano
e522a59a4c update docs 2022-11-24 13:07:25 -08:00
wobsoriano
1d7be4642d update docs 2022-11-24 13:07:13 -08:00
wobsoriano
3552017e4f update docs 2022-11-24 13:05:32 -08:00
wobsoriano
de690a7914 update docs 2022-11-24 13:00:40 -08:00
wobsoriano
d2666650de update docs 2022-11-24 12:57:29 -08:00
wobsoriano
e3d35c6b04 update docs 2022-11-24 12:56:59 -08:00
wobsoriano
b569afde50 update docs 2022-11-24 12:53:17 -08:00
wobsoriano
d4f3942fff update docs 2022-11-24 12:52:17 -08:00
wobsoriano
68003e9c3e update docs 2022-11-24 12:51:28 -08:00
Robert Soriano
b27938f108 Merge pull request #44 from benfavre/patch-1
Update package.json
2022-11-21 20:15:33 -08:00
Webdesign29
9e443ac559 Update package.json
Fix calling --watch for tsup when using "dev" command
2022-11-19 22:11:12 +01:00
wobsoriano
8ba002407a update lockfile 2022-11-17 02:44:00 -08:00
wobsoriano
f061b0531f chore: release v0.4.2 2022-11-17 02:42:20 -08:00
wobsoriano
f08b724df7 feat: get req,res values from event.node 2022-11-17 02:42:01 -08:00
wobsoriano
4085d5fe26 fix: request types 2022-11-17 02:41:01 -08:00
wobsoriano
13713343ce fix: request types 2022-11-17 02:39:45 -08:00
wobsoriano
d6f11bb301 feat(deps): bump h3 to 1.0.1 2022-11-17 02:38:27 -08:00
wobsoriano
c5c1f1cdba feat(deps): bump h3, ohash and ufo to 1.0.0 2022-11-17 02:38:17 -08:00
wobsoriano
0681dd554d Merge branch 'next' of https://github.com/wobsoriano/trpc-nuxt into next 2022-11-16 08:48:14 -08:00
wobsoriano
e7a76694af update docs 2022-11-16 08:48:09 -08:00
wobsoriano
da75c18ebe update local nuxt dep to stable 2022-11-16 08:42:45 -08:00
wobsoriano
6289575900 bump local trpc deps to rc.8 2022-11-16 08:40:35 -08:00
Robert Soriano
ac212672e4 Update 2.server-side-calls.md 2022-11-14 12:26:48 -08:00
Robert Soriano
b8e2a83e26 Merge pull request #42 from nkhdo/patch-1
Update 2.server-side-calls.md
2022-11-14 10:43:06 -08:00
Hoang Do
fd98b0d414 Update 2.server-side-calls.md
Fix wrong variable import
2022-11-15 00:46:06 +07:00
wobsoriano
20ab235856 docs: add auth tips 2022-11-13 11:18:57 -08:00
wobsoriano
7205c356f8 docs: update aborting procs 2022-11-13 11:08:28 -08:00
wobsoriano
e14827e0c7 docs: add aborting procs 2022-11-13 11:05:47 -08:00
wobsoriano
eea6406d5a docs: update example 2022-11-13 10:58:27 -08:00
wobsoriano
2d3409a25b docs: update example 2022-11-13 10:58:08 -08:00
wobsoriano
7a71a6e128 docs: add mutation example 2022-11-13 10:55:23 -08:00
wobsoriano
d8e89297cd docs: typo fix 2022-11-13 10:41:35 -08:00
wobsoriano
6768205318 docs: update composables 2022-11-13 10:41:22 -08:00
wobsoriano
c20a092a31 docs: add tips section 2022-11-13 10:37:24 -08:00
wobsoriano
64df8fb7ad infer error in playground 2022-11-13 09:43:23 -08:00
wobsoriano
d5567e5826 remove character 2022-11-12 21:16:19 -08:00
wobsoriano
6b898e836c update readme 2022-11-12 21:15:19 -08:00
wobsoriano
fd38c4f983 chore: release v0.4.1 2022-11-12 20:16:38 -08:00
wobsoriano
48d4c6342f update playground 2022-11-12 20:16:29 -08:00
wobsoriano
297c3d52f4 docs: update usage 2022-11-12 20:15:52 -08:00
wobsoriano
0f9653d1a4 remove /module 2022-11-12 20:12:36 -08:00
wobsoriano
85998101c7 feat: remove nuxt 3 wrappers for client 2022-11-12 20:10:50 -08:00
wobsoriano
d6cb770154 update installation instructions 2022-11-12 18:41:44 -08:00
wobsoriano
c222008524 update installation instructions 2022-11-12 18:41:16 -08:00
wobsoriano
9cd21eb300 update readme docs url 2022-11-12 18:40:15 -08:00
wobsoriano
b4e83dbb0b add back preset 2022-11-12 18:38:25 -08:00
wobsoriano
0fa63c86ba test node preset 2022-11-12 18:31:35 -08:00
wobsoriano
7371dc4c10 update docs 2022-11-12 18:28:26 -08:00
wobsoriano
77ab5f0dc6 reinstall deps 2022-11-12 18:20:07 -08:00
wobsoriano
ba15d15d67 remove patched @vueuse/head 2022-11-12 18:17:10 -08:00
wobsoriano
7999039088 update github config 2022-11-12 18:13:49 -08:00
wobsoriano
dc15500074 dont prerender index 2022-11-12 18:13:00 -08:00
wobsoriano
99a83562aa add tmp docs link 2022-11-12 18:09:34 -08:00
wobsoriano
e04344d508 docs: remove package manager 2022-11-12 18:08:22 -08:00
34 changed files with 2137 additions and 3203 deletions

View File

@@ -2,6 +2,17 @@
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications. End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
<p align="center">
<figure>
<img src="https://i.imgur.com/3AZlBZH.gif" alt="Demo" />
<figcaption>
<p align="center">
The client above is <strong>not</strong> importing any code from the server, only its type declarations.
</p>
</figcaption>
</figure>
</p>
Docs: https://trpc-nuxt.vercel.app Docs: https://trpc-nuxt.vercel.app
For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3). For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3).

View File

@@ -1,5 +1,25 @@
export default defineAppConfig({ export default defineAppConfig({
docus: { docus: {
title: 'tRPC Nuxt' title: 'tRPC Nuxt',
image: 'https://og-image.vercel.app/tRPC-Nuxt',
alt: 'tRPC-Nuxt cover',
url: 'https://trpc-nuxt.vercel.app',
debug: false,
socials: {
github: 'wobsoriano/trpc-nuxt'
},
aside: {
level: 1
},
footer: {
credits: true,
icons: [
{
label: 'NuxtJS',
href: 'https://nuxtjs.org',
component: 'IconNuxt'
}
]
}
} }
}) })

View File

@@ -5,24 +5,30 @@ description: tRPC-Nuxt provides first class integration with tRPC.
# Installation # Installation
## 1. Add to existing Nuxt project
::code-group ::code-group
```bash [pnpm] ```bash [pnpm]
pnpm add @trpc/server@next @trpc/client@next trpc-nuxt@beta zod pnpm add @trpc/server @trpc/client trpc-nuxt zod
``` ```
```bash [npm] ```bash [npm]
npm install @trpc/server@next @trpc/client@next trpc-nuxt@beta zod npm install @trpc/server @trpc/client trpc-nuxt zod
``` ```
```bash [yarn] ```bash [yarn]
yarn add @trpc/server@next @trpc/client@next trpc-nuxt@beta zod yarn add @trpc/server @trpc/client trpc-nuxt zod
``` ```
:: ::
```ts [nuxt.config.ts]
export default defineNuxtConfig({
build: {
transpile: ['trpc-nuxt/client']
}
})
```
#### Why @trpc/server? #### Why @trpc/server?
For implementing tRPC endpoints and routers. For implementing tRPC endpoints and routers.
@@ -35,18 +41,6 @@ For making typesafe API calls from your client.
Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required. Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required.
## 2. Install module
This will transpile `trpc-nuxt` and exclude `trpc-nuxt/client` from optimized dependencies by Vite.
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['trpc-nuxt/module']
})
```
::
## Next Steps ## Next Steps
Now that you've installed the required dependencies, you are ready to start building your application. Now that you've installed the required dependencies, you are ready to start building your application.

View File

@@ -0,0 +1,118 @@
---
title: Simple
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Simple Usage
## 1. Create a tRPC router
Initialize your tRPC backend using the `initTRPC` function and create your first router.
::code-group
```ts [server/trpc/trpc.ts]
/**
* This is your entry point to setup the root configuration for tRPC on the server.
* - `initTRPC` should only be used once per app.
* - We export only the functionality that we use so we can enforce which base procedures should be used
*
* Learn how to create protected base procedures and other things below:
* @see https://trpc.io/docs/v10/router
* @see https://trpc.io/docs/v10/procedures
*/
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
/**
* Unprotected procedure
**/
export const publicProcedure = t.procedure;
export const router = t.router;
export const middleware = t.middleware;
```
```ts [server/api/trpc/[trpc].ts]
/**
* This is the API-handler of your app that contains all your API routes.
* On a bigger app, you will probably want to split this file up into multiple files.
*/
import { createNuxtApiHandler } from 'trpc-nuxt'
import { publicProcedure, router } from '~/server/trpc/trpc'
import { z } from 'zod'
export const appRouter = router({
hello: publicProcedure
// This is the input schema of your procedure
.input(
z.object({
text: z.string().nullish(),
}),
)
.query(({ input }) => {
// This is what you're returning to your client
return {
greeting: `hello ${input?.text ?? 'world'}`,
}
}),
})
// export only the type definition of the API
// None of the actual implementation is exposed to the client
export type AppRouter = typeof appRouter;
// export API handler
export default createNuxtApiHandler({
router: appRouter,
createContext: () => ({}),
})
```
::
## 2. Create tRPC client plugin
Create a strongly-typed plugin using your API's type signature.
```ts [plugins/client.ts]
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~/server/trpc/routers'
export default defineNuxtPlugin(() => {
/**
* createTRPCNuxtClient adds a `useQuery` composable
* built on top of `useAsyncData`.
*/
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
return {
provide: {
client,
},
}
})
```
## 3. Make an API request
```vue [pages/index.vue]
<script setup lang="ts">
const { $client } = useNuxtApp()
const hello = await $client.hello.useQuery({ text: 'client' })
</script>
<template>
<div>
<p>{{ hello.data?.greeting }}</p>
</div>
</template>
```

View File

@@ -1,13 +1,11 @@
--- ---
title: Usage title: Recommended
description: tRPC-Nuxt provides first class integration with tRPC. description: tRPC-Nuxt provides first class integration with tRPC.
--- ---
# Usage # Recommended Usage
## Recommended file structure Recommended but not enforced file structure.
Recommended but not enforced file structure. This is what you get when starting from [the examples](../main/example-apps.md).
```graphql ```graphql
. .
@@ -24,7 +22,7 @@ Recommended but not enforced file structure. This is what you get when starting
│ │ ├── context.ts # <-- create app context │ │ ├── context.ts # <-- create app context
│ │ └── trpc.ts # <-- procedure helpers │ │ └── trpc.ts # <-- procedure helpers
├── plugins ├── plugins
│ ├── client.ts # <-- tRPC Client as a plugin │ ├── client.ts # <-- tRPC client plugin
└── [..] └── [..]
``` ```
@@ -35,16 +33,26 @@ Initialize your tRPC backend using the `initTRPC` function and create your first
::code-group ::code-group
```ts [server/trpc/trpc.ts] ```ts [server/trpc/trpc.ts]
/**
* This is your entry point to setup the root configuration for tRPC on the server.
* - `initTRPC` should only be used once per app.
* - We export only the functionality that we use so we can enforce which base procedures should be used
*
* Learn how to create protected base procedures and other things below:
* @see https://trpc.io/docs/v10/router
* @see https://trpc.io/docs/v10/procedures
*/
import { initTRPC } from '@trpc/server' import { initTRPC } from '@trpc/server'
import { Context } from '~/server/trpc/context'
// Avoid exporting the entire t-object since it's not very const t = initTRPC.context<Context>().create()
// descriptive and can be confusing to newcomers used to t
// meaning translation in i18n libraries.
const t = initTRPC.create()
// Base router and procedure helpers /**
export const router = t.router * Unprotected procedure
export const publicProcedure = t.procedure **/
export const publicProcedure = t.procedure;
export const router = t.router;
export const middleware = t.middleware;
``` ```
```ts [server/trpc/routers/index.ts] ```ts [server/trpc/routers/index.ts]
@@ -71,15 +79,28 @@ export type AppRouter = typeof appRouter
```ts [server/api/trpc/[trpc].ts] ```ts [server/api/trpc/[trpc].ts]
import { createNuxtApiHandler } from 'trpc-nuxt' import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '@/server/trpc/routers' import { appRouter } from '~/server/trpc/routers'
import { createContext } from '~/server/trpc/context'
// export API handler // export API handler
export default createNuxtApiHandler({ export default createNuxtApiHandler({
router: appRouter, router: appRouter,
createContext: () => ({}), createContext,
}) })
``` ```
```ts [server/trpc/context.ts]
import { inferAsyncReturnType } from '@trpc/server'
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export const createContext = () => {}
export type Context = inferAsyncReturnType<typeof createContext>;
```
:: ::
::alert{type=info} ::alert{type=info}
@@ -88,22 +109,21 @@ If you need to split your router into several subrouters, you can implement them
## 2. Create tRPC client plugin ## 2. Create tRPC client plugin
Create a set of strongly-typed composables using your API's type signature. Create a strongly-typed plugin using your API's type signature.
```ts [plugins/client.ts] ```ts [plugins/client.ts]
import { httpBatchLink } from '@trpc/client' import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client' import type { AppRouter } from '~/server/trpc/routers'
import type { AppRouter } from '@/server/trpc/routers'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const client = createTRPCNuxtProxyClient<AppRouter>({ /**
* createTRPCNuxtClient adds a `useQuery` composable
* built on top of `useAsyncData`.
*/
const client = createTRPCNuxtClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
/** url: '/api/trpc',
* If you want to use SSR, you need to use the server's full URL
* @link https://trpc.io/docs/ssr
**/
url: 'http://localhost:3000/api/trpc',
}), }),
], ],
}) })
@@ -116,24 +136,17 @@ export default defineNuxtPlugin(() => {
}) })
``` ```
## 3. Make API requests ## 3. Make an API request
```vue [pages/index.vue] ```vue [pages/index.vue]
<script setup lang="ts"> <script setup lang="ts">
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
// query and mutate uses useAsyncData under the hood const hello = await $client.hello.useQuery({ text: 'client' })
const { data, pending, error } = await $client.hello.query({ text: 'client' })
</script> </script>
<template> <template>
<div v-if="pending"> <div>
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else>
<p>{{ hello.data?.greeting }}</p> <p>{{ hello.data?.greeting }}</p>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,53 @@
---
title: Client
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Nuxt client
The magic of tRPC is making strongly typed API calls without relying on code generation. With full-stack TypeScript projects, you can directly import types from the server into the client! This is a vital part of how tRPC works.
## Initialize a tRPC client
Create a typesafe client via a Nuxt [plugin](https://nuxt.com/docs/guide/directory-structure/plugins) with the `createTRPCNuxtClient` method from `trpc-nuxt/client`, and add a `links` array with a [terminating link](https://trpc.io/docs/links#the-terminating-link). If you want to learn more about tRPC links, check out the docs [here](https://trpc.io/docs/links):
::alert{type="info"}
`createTRPCNuxtClient` extends [createTRPCProxyClient](https://trpc.io/docs/vanilla#initialize-a-trpc-client) and adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data).
::
```ts [plugins/client.ts]
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~/server/trpc/routers'
export default defineNuxtPlugin(() => {
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
return {
provide: {
client,
},
}
})
```
As you can see, we passed `AppRouter` as a type argument of `createTRPCNuxtClient`. This returns a strongly typed `client` instance, a proxy that mirrors the structure of your `AppRouter` on the client:
```vue [pages/index.vue]
<script setup lang="ts">
const { $client } = useNuxtApp()
const getUser = await $client.getUser.useQuery('id_bilbo');
// => { data: { id: 'id_bilbo', name: 'Bilbo' }, pending: false, error: false };
const bilbo = await $client.getUser.query('id_bilbo');
// => { id: 'id_bilbo', name: 'Bilbo' };
const frodo = await $client.createUser.mutate({ name: 'Frodo' });
// => { id: 'id_frodo', name: 'Frodo' };
</script>
```

View File

@@ -0,0 +1,58 @@
---
title: HTTP Link
description: httpLink is a terminating link that sends a tRPC operation to a tRPC procedure over HTTP.
---
# HTTP Link
`httpLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that sends a tRPC operation to a tRPC procedure over HTTP.
`httpLink` supports both POST and GET requests.
::alert{type="info"}
`httpLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders).
::
## Usage
You can import and add the `httpLink` to the `links` array as such:
```ts
import { createTRPCNuxtClient, httpLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~/server/trpc/routers'
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpLink({
url: '/api/trpc',
}),
],
})
```
## `httpLink` Options
The `httpLink` function takes an options object that has the `HTTPLinkOptions` shape.
```ts
export interface HTTPLinkOptions {
url: string;
/**
* Select headers to pass to `useRequestHeaders`.
*/
pickHeaders?: string[];
/**
* Add ponyfill for fetch.
*/
fetch?: typeof fetch;
/**
* Add ponyfill for AbortController
*/
AbortController?: typeof AbortController | null;
/**
* Headers to be set on outgoing requests or a callback that of said headers
* @link http://trpc.io/docs/v10/header
*/
headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);
}
```

View File

@@ -0,0 +1,88 @@
---
title: HTTP Batch Link
description: httpBatchLink is a terminating link that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure.
---
# HTTP Batch Link
`httpBatchLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure.
::alert{type="info"}
`httpBatchLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpBatchLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders).
::
## Usage
You can import and add the `httpBatchLink` to the `links` array as such:
```ts
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~/server/trpc/routers'
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
})
```
After that, you can make use of batching by setting all your procedures in a `Promise.all`. The code below will produce exactly one HTTP request and on the server exactly `one` database query:
```ts
const somePosts = await Promise.all([
$client.post.byId.query(1);
$client.post.byId.query(2);
$client.post.byId.query(3);
])
```
## `httpBatchLink` Options
The `httpBatchLink` function takes an options object that has the `HTTPBatchLinkOptions` shape.
```ts
export interface HttpBatchLinkOptions extends HTTPLinkOptions {
maxURLLength?: number;
}
export interface HTTPLinkOptions {
url: string;
/**
* Select headers to pass to `useRequestHeaders`.
*/
pickHeaders?: string[];
/**
* Add ponyfill for fetch.
*/
fetch?: typeof fetch;
/**
* Add ponyfill for AbortController
*/
AbortController?: typeof AbortController | null;
/**
* Headers to be set on outgoing requests or a callback that of said headers
* @link http://trpc.io/docs/v10/header
*/
headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);
}
```
## Setting a maximum URL length
When sending batch requests, sometimes the URL can become too large causing HTTP errors like [413 Payload Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413), [414 URI Too Long](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414), and [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404). The `maxURLLength` option will limit the number of requests that can be sent together in a batch.
```ts
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client';
import type { AppRouter } from '~/server/trpc/routers'
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
maxURLLength: 2083, // a suitable size
}),
],
});
```

View File

@@ -0,0 +1,44 @@
---
title: Composables
---
# Composables
It is often useful to wrap functionality of your `@trpc/client` api within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your `@trpc/server` router.
::alert{type="info"}
[createTRPCNuxtClient](/get-started/client/create) adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data/).
::
## Inference Helpers
`@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router:
- `inferRouterInputs<TRouter>`
- `inferRouterOutputs<TRouter>`
Let's assume we have this example query wrapped within Nuxt's [useAsyncData](https://v3.nuxtjs.org/api/composables/use-async-data/):
```ts
const { data, error } = await useAsyncData(() => $client.todo.getTodos.query())
```
We can wrap this in a composable and also set the client error types:
```ts [composables/useGetTodos.ts]
import { TRPCClientError } from '@trpc/client'
import type { inferRouterOutputs } from '@trpc/server'
import type { AppRouter } from '@/server/trpc/routers'
type RouterOutput = inferRouterOutputs<AppRouter>
type GetTodosOutput = RouterOutput['todo']['getTodos']
type ErrorOutput = TRPCClientError<AppRouter>
export default function useGetTodos() {
const { $client } = useNuxtApp()
return useAsyncData<GetTodosOutput, ErrorOutput>(() => $client.todo.getTodos.query())
}
```
Now, we have a fully-typed composable.

View File

@@ -0,0 +1,44 @@
---
title: Headers
---
# Headers
We can use the built-in [useRequestHeaders](https://v3.nuxtjs.org/api/composables/use-request-headers/) to set outgoing request headers:
::alert{type="info"}
[createTRPCNuxtClient](/get-started/client/create) has this feature by default.
::
```ts [plugins/client.ts]
export default defineNuxtPlugin(() => {
const headers = useRequestHeaders()
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
// headers need to be a function so it gets called dynamically
// every HTTP request
headers() {
// You can add more custom headers here
return headers
}
}),
],
})
return {
provide: {
client,
},
}
})
```
```ts [server/trpc/context.ts]
export function createContext (event: H3Event) {
console.log('cookies', parseCookies(event))
return {}
}
```

View File

@@ -0,0 +1,103 @@
---
title: Authorization
---
# Authorization
The `createContext` function is called for each incoming request so here you can add contextual information about the calling user from the request object.
::alert{type="warning"}
Before you can access request headers in any context or middleware, you need to set the outgoing request headers. See [here](/get-started/tips/headers).
::
## Create context from request headers
```ts [server/trpc/context.ts]
import { inferAsyncReturnType } from '@trpc/server'
import { decodeAndVerifyJwtToken } from './somewhere/in/your/app/utils'
export async function createContext(event: H3Event) {
// Create your context based on the request object
// Will be available as `ctx` in all your resolvers
// This is just an example of something you might want to do in your ctx fn
const authorization = getRequestHeader(event, 'authorization')
async function getUserFromHeader() {
if (authorization) {
const user = await decodeAndVerifyJwtToken(authorization.split(' ')[1])
return user
}
return null
}
const user = await getUserFromHeader()
return {
user,
}
}
type Context = inferAsyncReturnType<typeof createContext>
```
## Option 1: Authorize using resolver
```ts
import { TRPCError, initTRPC } from '@trpc/server'
import type { Context } from '../context'
export const t = initTRPC.context<Context>().create()
const appRouter = t.router({
// open for anyone
hello: t.procedure
.input(z.string().nullish())
.query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`),
// checked in resolver
secret: t.procedure.query(({ ctx }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return {
secret: 'sauce',
}
}),
})
```
## Option 2: Authorize using middleware
```ts
import { TRPCError, initTRPC } from '@trpc/server'
export const t = initTRPC.context<Context>().create()
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: 'UNAUTHORIZED' })
}
return next({
ctx: {
user: ctx.user,
},
})
})
// you can reuse this for any procedure
export const protectedProcedure = t.procedure.use(isAuthed)
t.router({
// this is accessible for everyone
hello: t.procedure
.input(z.string().nullish())
.query(({ input, ctx }) => `hello ${input ?? ctx.user?.name ?? 'world'}`),
admin: t.router({
// this is accessible only to admins
secret: protectedProcedure.query(({ ctx }) => {
return {
secret: 'sauce',
}
}),
}),
})
```
This page is entirely based on [authorization docs](https://trpc.io/docs/v10/authorization) of tRPC with a minimal change made to work with Nuxt.

View File

@@ -0,0 +1,84 @@
---
title: Server Side Calls
---
# Server Side Calls
You may need to call your procedure(s) directly from the server, `createCaller()` function returns you an instance of `RouterCaller` able to execute queries and mutations.
## Input query example
We create the router with a input query and then we call the asynchronous `greeting` procedure to get the result.
::code-group
```ts [server/trpc/trpc.ts]
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const router = t.router({
// Create procedure at path 'greeting'
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query(({ input }) => `Hello ${input.name}`),
})
```
```ts [server/api/greeting.ts]
import { router } from '@/server/trpc/trpc'
export default eventHandler(async (event) => {
const { name } = getQuery(event)
const caller = router.createCaller({})
const greeting = await caller.greeting({ name })
return {
greeting
}
})
```
::
## Mutation example
We create the router with a mutation and then we call the asynchronous `post` procedure to get the result.
::code-group
```ts [server/trpc/trpc.ts]
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const posts = ['One', 'Two', 'Three']
const t = initTRPC.create()
export const router = t.router({
post: t.router({
add: t.procedure.input(z.string()).mutation(({ input }) => {
posts.push(input)
return posts
}),
}),
})
```
```ts [server/api/post.ts]
import { router } from '@/server/trpc/trpc'
export default eventHandler(async (event) => {
const body = await getBody(event)
const caller = router.createCaller({})
const post = await caller.post.add(body.post)
return {
post
}
})
```
::

View File

@@ -0,0 +1,24 @@
---
title: Aborting Procedures
---
# Aborting Procedures
tRPC adheres to the industry standard when it comes to aborting procedures. All you have to do is pass an `AbortSignal` to the query-options and then call its parent `AbortController`'s `abort` method.
```ts [composables/useGetTodo.ts]
export default function useGetTodo(id: number) {
const { $client } = useNuxtApp()
const ac = new AbortController()
onScopeDispose(() => {
ac.abort()
})
return useAsyncData(() => {
return $client.todo.getTodo.query(id, {
signal: ac.signal
})
})
}
```

View File

@@ -0,0 +1,4 @@
---
navigation: false
redirect: /get-started/installation
---

View File

@@ -1,6 +1,6 @@
--- ---
title: "tRPC Nuxt" title: "tRPC Nuxt"
description: "A supa simple wrapper arousnd supabase-js to enable usage and integration within Nuxt." description: "End-to-end typesafe APIs in Nuxt applications."
navigation: false navigation: false
layout: page layout: page
--- ---
@@ -13,7 +13,7 @@ cta:
secondary: secondary:
- Star on GitHub -> - Star on GitHub ->
- https://github.com/wobsoriano/trpc-nuxt - https://github.com/wobsoriano/trpc-nuxt
snippet: npm install trpc-nuxt@beta snippet: npm install trpc-nuxt
--- ---
#title #title
@@ -26,6 +26,6 @@ End-to-end typesafe APIs in Nuxt applications.
::list ::list
- Automatic typesafety - Automatic typesafety
- Snappy DX - Snappy DX
- Autocompletion - Autocompletion on the client, for inputs, outputs and errors
:: ::
:: ::

View File

@@ -6,9 +6,9 @@ export default defineNuxtConfig({
modules: ['@nuxtlabs/github-module'], modules: ['@nuxtlabs/github-module'],
extends: process.env.DOCUS_THEME_PATH || '@nuxt-themes/docus', extends: process.env.DOCUS_THEME_PATH || '@nuxt-themes/docus',
github: { github: {
owner: 'nuxt', owner: 'wobsoriano',
repo: 'content', repo: 'trpc-nuxt',
branch: 'main' branch: 'next'
}, },
colorMode: { colorMode: {
preference: 'dark' preference: 'dark'
@@ -17,10 +17,6 @@ export default defineNuxtConfig({
transpile: [/content-edge/, /github-module/] transpile: [/content-edge/, /github-module/]
}, },
nitro: { nitro: {
prerender: {
crawlLinks: true,
routes: ['/']
},
preset: 'vercel' preset: 'vercel'
} }
}) })

View File

@@ -8,12 +8,10 @@
"preview": "nuxi preview" "preview": "nuxi preview"
}, },
"dependencies": { "dependencies": {
"nuxt": "^3.0.0-rc.13", "nuxt": "^3.0.0"
"@nuxtjs/tailwindcss": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@nuxt-themes/docus": "npm:@nuxt-themes/docus-edge@latest", "@nuxt-themes/docus": "^1.1.10",
"@nuxtlabs/github-module": "npm:@nuxtlabs/github-module-edge@latest" "@nuxtlabs/github-module": "^1.5.4"
}, }
"packageManager": "yarn@1.22.19"
} }

View File

@@ -1,14 +1,6 @@
import { defineTheme } from 'pinceau' import { defineTheme } from 'pinceau'
export default defineTheme({ export default defineTheme({
title: 'tRPC-Nuxt',
cover: {
src: 'https://og-image.vercel.app/tRPC-Nuxt',
alt: 'tRPC-Nuxt conver'
},
aside: {
level: 1
},
colors: { colors: {
primary: { primary: {
50: '#BFEDFC', 50: '#BFEDFC',

View File

@@ -1,17 +0,0 @@
import { defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
meta: {
name: 'trpc-nuxt',
configKey: 'trpc',
compatibility: {
nuxt: '^3.0.0-rc.13'
}
},
setup (_moduleOptions, nuxt) {
nuxt.options.build.transpile.push('trpc-nuxt')
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
nuxt.options.vite.optimizeDeps.exclude = nuxt.options.vite.optimizeDeps.exclude || []
nuxt.options.vite.optimizeDeps.exclude.push('trpc-nuxt/client')
}
})

View File

@@ -1,21 +1,20 @@
{ {
"name": "trpc-nuxt", "name": "trpc-nuxt",
"description": "End-to-end typesafe APIs in Nuxt applications.",
"type": "module", "type": "module",
"version": "0.4.0", "packageManager": "pnpm@7.18.2",
"version": "0.4.4",
"license": "MIT", "license": "MIT",
"sideEffects": false, "sideEffects": false,
"exports": { "exports": {
"./package.json": "./package.json",
".": { ".": {
"require": "./dist/index.cjs", "require": "./dist/index.cjs",
"import": "./dist/index.mjs" "import": "./dist/index.mjs"
}, },
"./client": { "./client": {
"types": "./dist/client/index.d.ts",
"require": "./dist/client/index.cjs", "require": "./dist/client/index.cjs",
"import": "./dist/client/index.mjs" "import": "./dist/client/index.mjs"
},
"./module": {
"import": "./module.mjs"
} }
}, },
"main": "./dist/index.mjs", "main": "./dist/index.mjs",
@@ -23,11 +22,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": [ "files": [
"dist", "dist",
"client.d.ts", "client.d.ts"
"module.mjs"
], ],
"scripts": { "scripts": {
"dev": "concurrently \"pnpm build --watch\" \"pnpm --filter playground dev\"", "dev": "concurrently \"pnpm build -- --watch\" \"pnpm --filter playground dev\"",
"dev:prepare": "pnpm build && nuxt prepare playground", "dev:prepare": "pnpm build && nuxt prepare playground",
"prepublishOnly": "pnpm build", "prepublishOnly": "pnpm build",
"build": "tsup", "build": "tsup",
@@ -36,35 +34,28 @@
"release": "bumpp && npm publish" "release": "bumpp && npm publish"
}, },
"peerDependencies": { "peerDependencies": {
"@trpc/client": "^10.0.0-proxy-beta.21", "@trpc/client": "^10.0.0",
"@trpc/server": "^10.0.0-proxy-beta.21" "@trpc/server": "^10.0.0"
}, },
"dependencies": { "dependencies": {
"h3": "^0.8.6", "h3": "^1.0.2",
"nanoid": "^4.0.0", "ofetch": "^1.0.0",
"nuxt": "^3.0.0-rc.13", "ohash": "^1.0.0",
"ohash": "^0.1.5", "ufo": "^1.0.0"
"ufo": "^0.8.6"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/kit": "3.0.0-rc.13", "@nuxt/eslint-config": "^0.1.1",
"@nuxtjs/eslint-config-typescript": "^11.0.0", "@trpc/client": "^10.5.0",
"@trpc/client": "10.0.0-rc.7", "@trpc/server": "^10.5.0",
"@trpc/server": "10.0.0-rc.7",
"bumpp": "^8.2.1", "bumpp": "^8.2.1",
"concurrently": "^7.5.0", "concurrently": "^7.5.0",
"eslint": "^8.25.0", "eslint": "^8.25.0",
"tsup": "6.4.0", "tsup": "6.4.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"pnpm": {
"patchedDependencies": {
"nuxt@3.0.0-rc.13": "patches/nuxt@3.0.0-rc.13.patch"
}
},
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"@nuxtjs/eslint-config-typescript" "@nuxt/eslint-config"
], ],
"rules": { "rules": {
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
@@ -77,6 +68,16 @@
"eslintIgnore": [ "eslintIgnore": [
"*.json", "*.json",
"node_modules", "node_modules",
"*.md" "*.md",
] "dist",
".output"
],
"pnpm": {
"overrides": {
"nuxt": "3.0.0"
}
},
"engines": {
"node": "^16.13.0 || ^18.12.0"
}
} }

View File

@@ -1,22 +0,0 @@
diff --git a/dist/head/runtime/lib/vueuse-head.plugin.mjs b/dist/head/runtime/lib/vueuse-head.plugin.mjs
index a3278e56986d8e8efb099c502c82d9661a36d846..148a8879ba433072cecd15995cfd5b1b69941124 100644
--- a/dist/head/runtime/lib/vueuse-head.plugin.mjs
+++ b/dist/head/runtime/lib/vueuse-head.plugin.mjs
@@ -4,7 +4,7 @@ import { defineNuxtPlugin, useRouter } from "#app";
import { appHead } from "#build/nuxt.config.mjs";
export default defineNuxtPlugin((nuxtApp) => {
const head = createHead();
- head.addEntry(appHead, { resolved: true });
+ head.addHeadObjs(appHead, { resolved: true });
nuxtApp.vueApp.use(head);
if (process.client) {
let pauseDOMUpdates = true;
@@ -25,7 +25,7 @@ export default defineNuxtPlugin((nuxtApp) => {
}
nuxtApp._useHead = (_meta, options) => {
if (process.server) {
- head.addEntry(_meta, options);
+ head.addHeadObjs(_meta, options);
return;
}
const cleanUp = head.addReactiveEntry(_meta, options);

View File

@@ -1,4 +1,6 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['trpc-nuxt/module'] build: {
transpile: ['trpc-nuxt/client']
}
}) })

View File

@@ -9,13 +9,13 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@trpc/client": "10.0.0-rc.7", "@trpc/client": "^10.5.0",
"@trpc/server": "10.0.0-rc.7", "@trpc/server": "^10.5.0",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"trpc-nuxt": "workspace:*", "trpc-nuxt": "workspace:*",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"nuxt": "^3.0.0-rc.13" "nuxt": "^3.0.0"
} }
} }

View File

@@ -1,11 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
// const headers = useClientHeaders()
// const addHeader = () => {
// headers.value.authorization = 'Bearer abcdefghijklmnop'
// console.log(headers.value)
// }
const addTodo = async () => { const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7) const title = Math.random().toString(36).slice(2, 7)
@@ -17,17 +11,13 @@ const addTodo = async () => {
title, title,
completed: false completed: false
}) })
console.log(x.data.value) console.log(x)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
} }
const { data: todos, pending, error, refresh } = await $client.todo.getTodos.query(undefined, { const { data: todos, pending, error, refresh } = await $client.todo.getTodos.useQuery()
trpc: {
abortOnUnmount: true
}
})
</script> </script>
<template> <template>
@@ -40,8 +30,14 @@ const { data: todos, pending, error, refresh } = await $client.todo.getTodos.que
</div> </div>
<div v-else> <div v-else>
<ul> <ul>
<li v-for="t in todos?.slice(0, 10)" :key="t.id"> <li
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`"> v-for="t in todos?.slice(0, 10)"
:key="t.id"
>
<NuxtLink
:class="{ completed: t.completed }"
:to="`/todo/${t.id}`"
>
Title: {{ t.title }} Title: {{ t.title }}
</NuxtLink> </NuxtLink>
</li> </li>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
const { data: todo, pending, error } = await $client.todo.getTodo.query(Number(route.params.id)) const { data: todo, pending, error } = await useAsyncData(() => $client.todo.getTodo.query(Number(route.params.id)))
</script> </script>
<template> <template>

View File

@@ -1,10 +1,10 @@
import { httpBatchLink, loggerLink } from '@trpc/client' import { loggerLink } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import superjson from 'superjson' import superjson from 'superjson'
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
import type { AppRouter } from '~~/server/trpc/routers' import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const client = createTRPCNuxtProxyClient<AppRouter>({ const client = createTRPCNuxtClient<AppRouter>({
transformer: superjson, transformer: superjson,
links: [ links: [
// adds pretty logs to your console in development and logs errors in production // adds pretty logs to your console in development and logs errors in production
@@ -13,9 +13,7 @@ export default defineNuxtPlugin(() => {
process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error) (opts.direction === 'down' && opts.result instanceof Error)
}), }),
httpBatchLink({ httpBatchLink()
url: 'http://localhost:3000/api/trpc'
})
] ]
}) })

View File

@@ -9,9 +9,10 @@ export type Context = inferAsyncReturnType<typeof createContext>
* @link https://trpc.io/docs/context * @link https://trpc.io/docs/context
*/ */
export function createContext ( export function createContext (
opts: H3Event event: H3Event
) { ) {
// for API-response caching see https://trpc.io/docs/caching // for API-response caching see https://trpc.io/docs/caching
console.log('cookies', parseCookies(event))
return {} return {}
} }

3960
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,8 @@
import type { CreateTRPCClientOptions, inferRouterProxyClient } from '@trpc/client' import { type CreateTRPCClientOptions, type inferRouterProxyClient, createTRPCProxyClient } from '@trpc/client'
import { createTRPCProxyClient } from '@trpc/client' import { type AnyRouter } from '@trpc/server'
import type {
AnyRouter
} from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared' import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash' import { hash } from 'ohash'
import { nanoid } from 'nanoid' import { type DecoratedProcedureRecord } from './types'
import type { DecoratedProcedureRecord } from './types'
// @ts-expect-error: Nuxt auto-imports // @ts-expect-error: Nuxt auto-imports
import { getCurrentInstance, onScopeDispose, useAsyncData } from '#imports' import { getCurrentInstance, onScopeDispose, useAsyncData } from '#imports'
@@ -20,43 +16,47 @@ export function getQueryKey (
return input === undefined ? path : `${path}-${hash(input || '')}` return input === undefined ? path : `${path}-${hash(input || '')}`
} }
function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) { export function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => { return createRecursiveProxy((opts) => {
const args = opts.args const args = opts.args
const pathCopy = [name, ...opts.path] const pathCopy = [name, ...opts.path]
// The last arg is for instance `.mutate` or `.query()` // The last arg is for instance `.useMutation` or `.useQuery()`
const lastArg = pathCopy.pop()! const lastArg = pathCopy.pop()!
// The `path` ends up being something like `post.byId`
const path = pathCopy.join('.') const path = pathCopy.join('.')
const [input, otherOptions] = args const [input, otherOptions] = args
const queryKey = lastArg === 'mutate' ? nanoid() : getQueryKey(path, input) if (lastArg === 'useQuery') {
const { trpc, ...asyncDataOptions } = otherOptions || {} as any
const { trpc, ...asyncDataOptions } = otherOptions || {} as any let controller: AbortController
let controller: AbortController if (trpc?.abortOnUnmount) {
if (getCurrentInstance()) {
if (trpc?.abortOnUnmount) { onScopeDispose(() => {
if (getCurrentInstance()) { controller?.abort?.()
onScopeDispose(() => { })
controller?.abort?.() }
}) controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
} }
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
const queryKey = getQueryKey(path, input)
return useAsyncData(queryKey, () => (client as any)[path].query(input, {
signal: controller?.signal,
...trpc
}), asyncDataOptions)
} }
return useAsyncData(queryKey, () => (client as any)[path][lastArg](input, { return (client as any)[path][lastArg](input)
signal: controller?.signal,
...trpc
}), asyncDataOptions)
}) })
} }
export function createTRPCNuxtProxyClient<TRouter extends AnyRouter> (opts: CreateTRPCClientOptions<TRouter>) { export function createTRPCNuxtClient<TRouter extends AnyRouter> (opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient(opts) const client = createTRPCProxyClient<TRouter>(opts)
const decoratedClient = createFlatProxy((key) => { const decoratedClient = createFlatProxy((key) => {
return createNuxtProxyDecoration(key, client) return createNuxtProxyDecoration(key, client)
@@ -64,3 +64,8 @@ export function createTRPCNuxtProxyClient<TRouter extends AnyRouter> (opts: Crea
return decoratedClient return decoratedClient
} }
export {
httpBatchLink,
httpLink
} from './links'

78
src/client/links.ts Normal file
View File

@@ -0,0 +1,78 @@
import { httpLink as _httpLink, httpBatchLink as _httpBatchLink } from '@trpc/client'
import { type AnyRouter } from '@trpc/server'
import { FetchError } from 'ofetch'
// @ts-expect-error: Nuxt auto-imports
import { useRequestHeaders } from '#imports'
import { type HTTPLinkOptions as _HTTPLinkOptions } from '@trpc/client/dist/links/internals/httpUtils'
function customFetch(input: RequestInfo | URL, init?: RequestInit) {
return globalThis.$fetch.raw(input.toString(), init)
.catch((e) => {
if (e instanceof FetchError && e.response) { return e.response }
throw e
})
.then(response => ({
...response,
json: () => Promise.resolve(response._data)
}))
}
export interface HTTPLinkOptions extends _HTTPLinkOptions {
/**
* Select headers to pass to `useRequestHeaders`.
*/
pickHeaders?: string[]
}
/**
* This is a convenience wrapper around the original httpLink
* that replaces regular `fetch` with a `$fetch` from Nuxt. It
* also sets the default headers based on `useRequestHeaders` values.
*
* During server-side rendering, calling $fetch to fetch your internal API routes
* will directly call the relevant function (emulating the request),
* saving an additional API call.
*
* @see https://nuxt.com/docs/api/utils/dollarfetch
*/
export function httpLink<TRouter extends AnyRouter>(opts?: HTTPLinkOptions) {
const headers = useRequestHeaders(opts?.pickHeaders)
return _httpLink<TRouter>({
url: '/api/trpc',
headers () {
return headers
},
fetch: customFetch,
...opts,
})
}
export interface HttpBatchLinkOptions extends HTTPLinkOptions {
maxURLLength?: number;
}
/**
* This is a convenience wrapper around the original httpBatchLink
* that replaces regular `fetch` with a `$fetch` from Nuxt. It
* also sets the default headers based on `useRequestHeaders` values.
*
* During server-side rendering, calling $fetch to fetch your internal API routes
* will directly call the relevant function (emulating the request),
* saving an additional API call.
*
* @see https://nuxt.com/docs/api/utils/dollarfetch
*/
export function httpBatchLink<TRouter extends AnyRouter>(opts?: HttpBatchLinkOptions) {
const headers = useRequestHeaders(opts?.pickHeaders)
return _httpBatchLink<TRouter>({
url: '/api/trpc',
headers () {
return headers
},
fetch: customFetch,
...opts,
})
}

View File

@@ -1,17 +1,18 @@
import type { TRPCClientErrorLike, TRPCRequestOptions } from '@trpc/client' import type { TRPCClientErrorLike, TRPCRequestOptions as _TRPCRequestOptions } from '@trpc/client'
import type { TRPCSubscriptionObserver } from '@trpc/client/dist/internals/TRPCClient' import { type TRPCSubscriptionObserver } from '@trpc/client/dist/internals/TRPCClient'
import type { import type {
AnyMutationProcedure, AnyMutationProcedure,
AnyProcedure, AnyProcedure,
AnyQueryProcedure, AnyQueryProcedure,
AnyRouter, AnyRouter,
AnySubscriptionProcedure,
ProcedureArgs,
ProcedureRouterRecord, ProcedureRouterRecord,
inferProcedureInput, inferProcedureInput,
inferProcedureOutput inferProcedureOutput,
ProcedureArgs,
AnySubscriptionProcedure
} from '@trpc/server' } from '@trpc/server'
import type { Unsubscribable, inferObservableValue } from '@trpc/server/observable' import { type inferObservableValue, type Unsubscribable } from '@trpc/server/observable'
import { inferTransformedProcedureOutput } from '@trpc/server/shared'
import type { import type {
AsyncData, AsyncData,
AsyncDataOptions, AsyncDataOptions,
@@ -20,14 +21,14 @@ import type {
_Transform _Transform
} from 'nuxt/dist/app/composables/asyncData' } from 'nuxt/dist/app/composables/asyncData'
// Modified @trpc/client and @trpc/react-query types interface TRPCRequestOptions extends _TRPCRequestOptions {
// https://github.com/trpc/trpc/blob/next/packages/client/src/createTRPCClientProxy.ts
// https://github.com/trpc/trpc/blob/next/packages/react-query/src/createTRPCReact.tsx
interface TRPCOptions extends TRPCRequestOptions {
abortOnUnmount?: boolean abortOnUnmount?: boolean
} }
type Resolver<TProcedure extends AnyProcedure> = (
...args: ProcedureArgs<TProcedure['_def']>
) => Promise<inferTransformedProcedureOutput<TProcedure>>;
type SubscriptionResolver< type SubscriptionResolver<
TProcedure extends AnyProcedure, TProcedure extends AnyProcedure,
TRouter extends AnyRouter, TRouter extends AnyRouter,
@@ -49,23 +50,17 @@ type DecorateProcedure<
TRouter extends AnyRouter, TRouter extends AnyRouter,
> = TProcedure extends AnyQueryProcedure > = TProcedure extends AnyQueryProcedure
? { ? {
query: < useQuery: <
TData = inferProcedureOutput<TProcedure>, TData = inferTransformedProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>, Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>, PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>( >(
input: inferProcedureInput<TProcedure>, input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc: TRPCOptions }, opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc?: TRPCRequestOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>> ) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>,
query: Resolver<TProcedure>
} : TProcedure extends AnyMutationProcedure ? { } : TProcedure extends AnyMutationProcedure ? {
mutate: < mutate: Resolver<TProcedure>
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc: TRPCOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : TProcedure extends AnySubscriptionProcedure ? { } : TProcedure extends AnySubscriptionProcedure ? {
subscribe: SubscriptionResolver<TProcedure, TRouter> subscribe: SubscriptionResolver<TProcedure, TRouter>
} : never } : never

View File

@@ -1 +1,126 @@
export * from './server' import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
inferRouterContext,
inferRouterError
} from '@trpc/server'
import {
TRPCError
} from '@trpc/server'
import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { createError, defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
ctx?: inferRouterContext<TRouter>
paths?: string[]
type: ProcedureType | 'unknown'
errors: TRPCError[]
}
export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: ResponseMetaFnPayload<TRouter>) => ResponseMeta
export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: H3Event['node']['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export interface ResolveHTTPRequestOptions<TRouter extends AnyRouter> {
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
batching?: {
enabled: boolean
}
}
function getPath (event: H3Event): string | null {
if (typeof event.context.params.trpc === 'string') { return event.context.params.trpc }
if (Array.isArray(event.context.params.trpc)) { return event.context.params.trpc.join('/') }
return null
}
export function createNuxtApiHandler<TRouter extends AnyRouter> ({
router,
createContext,
responseMeta,
onError,
batching
}: ResolveHTTPRequestOptions<TRouter>) {
return defineEventHandler(async (event) => {
const {
req,
res
} = event.node
const $url = createURL(req.url!)
const path = getPath(event)
if (path === null) {
const error = router.getErrorShape({
error: new TRPCError({
message:
'Param "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR'
}),
type: 'unknown',
ctx: undefined,
path: undefined,
input: undefined
})
throw createError({
statusCode: 500,
statusMessage: JSON.stringify(error)
})
}
const httpResponse = await resolveHTTPResponse({
batching,
router,
req: {
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams
},
path,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req
})
}
})
const { status, headers, body } = httpResponse
res.statusCode = status
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body
})
}

View File

@@ -1,126 +0,0 @@
import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
inferRouterContext,
inferRouterError
} from '@trpc/server'
import {
TRPCError
} from '@trpc/server'
import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { createError, defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
ctx?: inferRouterContext<TRouter>
paths?: string[]
type: ProcedureType | 'unknown'
errors: TRPCError[]
}
export type ResponseMetaFn<TRouter extends AnyRouter> = (opts: ResponseMetaFnPayload<TRouter>) => ResponseMeta
export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: H3Event['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export interface ResolveHTTPRequestOptions<TRouter extends AnyRouter> {
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
batching?: {
enabled: boolean
}
}
function getPath (event: H3Event): string | null {
if (typeof event.context.params.trpc === 'string') { return event.context.params.trpc }
if (Array.isArray(event.context.params.trpc)) { return event.context.params.trpc.join('/') }
return null
}
export function createNuxtApiHandler<TRouter extends AnyRouter> ({
router,
createContext,
responseMeta,
onError,
batching
}: ResolveHTTPRequestOptions<TRouter>) {
return defineEventHandler(async (event) => {
const {
req,
res
} = event
const $url = createURL(req.url!)
const path = getPath(event)
if (path === null) {
const error = router.getErrorShape({
error: new TRPCError({
message:
'Param "trpc" not found - is the file named `[trpc]`.ts or `[...trpc].ts`?',
code: 'INTERNAL_SERVER_ERROR'
}),
type: 'unknown',
ctx: undefined,
path: undefined,
input: undefined
})
throw createError({
statusCode: 500,
statusMessage: JSON.stringify(error)
})
}
const httpResponse = await resolveHTTPResponse({
batching,
router,
req: {
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams
},
path,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req
})
}
})
const { status, headers, body } = httpResponse
res.statusCode = status
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body
})
}

View File

@@ -5,11 +5,11 @@ export default defineConfig({
format: ['cjs', 'esm'], format: ['cjs', 'esm'],
splitting: false, splitting: false,
clean: true, clean: true,
external: ['#app', '#imports'], external: ['#app', '#imports', /@trpc\/client/, /@trpc\/server/],
dts: true, dts: true,
outExtension({ format }) { outExtension ({ format }) {
return { return {
js: format === 'esm' ? '.mjs' : `.${format}`, js: format === 'esm' ? '.mjs' : `.${format}`
} }
}, }
}) })