Compare commits

...

180 Commits
v3 ... v0.4.3

Author SHA1 Message Date
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
wobsoriano
0f9d26ab60 chore: release v0.4.0 2022-11-12 18:01:55 -08:00
wobsoriano
e6b4386bd8 remove tag 2022-11-12 18:01:10 -08:00
wobsoriano
813b9ff7d7 move eslint config to package.json 2022-11-12 17:55:32 -08:00
wobsoriano
5dce1f4681 update readme 2022-11-12 17:54:17 -08:00
wobsoriano
470ba96be2 transpile github module 2022-11-12 16:32:18 -08:00
wobsoriano
4532add2a7 docs: patch vueuse/head 2022-11-12 16:26:43 -08:00
wobsoriano
876ed1d8d2 bump local trpc deps to rc.7 2022-11-08 09:30:58 -08:00
wobsoriano
611e22d205 delete merge conflicts 2022-11-08 09:27:22 -08:00
wobsoriano
c1857f1079 eslint fix 2022-11-08 09:25:21 -08:00
wobsoriano
796fa43a83 fix lockfile 2022-11-08 09:23:14 -08:00
wobsoriano
e3ce0bcaae merge conflicts 2022-11-08 09:22:30 -08:00
wobsoriano
e916f17d60 update docs 2022-11-08 09:06:08 -08:00
wobsoriano
0fdb9e0e17 update colors 2022-11-08 09:05:36 -08:00
wobsoriano
5bd5756d26 update og image 2022-11-08 09:00:41 -08:00
wobsoriano
67f95756ab fix docs 2022-11-08 08:59:32 -08:00
wobsoriano
1fc4d9d0d0 Revert "feat: add file routing option"
This reverts commit 750783e860.
2022-11-05 21:24:24 -07:00
wobsoriano
c11a5cfa8a remove examples folder 2022-11-05 20:13:48 -07:00
wobsoriano
92c9f6390c disable file routing by default 2022-11-05 20:06:02 -07:00
wobsoriano
3a6c5b659b update function names 2022-11-05 19:50:39 -07:00
wobsoriano
750783e860 feat: add file routing option 2022-11-05 19:22:19 -07:00
wobsoriano
d9c8f877d3 eslint fixes 2022-11-05 12:57:45 -07:00
wobsoriano
e359702c41 eslint fixes 2022-11-05 11:56:14 -07:00
wobsoriano
e9081d00ad replace eslint config 2022-11-05 11:49:46 -07:00
wobsoriano
83f98e34fa more eslint fixes 2022-11-04 08:04:58 -07:00
wobsoriano
512a0ae7b1 more eslint fixes 2022-11-04 08:04:46 -07:00
wobsoriano
c89f8747ea bump local deps 2022-11-04 08:04:28 -07:00
wobsoriano
3e8a2de6e5 more eslint fixes 2022-11-04 08:02:52 -07:00
wobsoriano
5d98873cc3 eslint fix 2022-11-04 08:01:24 -07:00
wobsoriano
d6012eb5c8 feat: import vue functions from #imports 2022-11-04 07:49:37 -07:00
wobsoriano
c1ab3ecf77 update scripts 2022-11-04 07:44:09 -07:00
wobsoriano
0e8f6cf9f6 update playground 2022-11-04 07:41:27 -07:00
wobsoriano
0f5c09c7e0 update docs 2022-11-04 07:40:49 -07:00
wobsoriano
5080e9a4a7 bump local trpc deps to 10.0.0-rc.4 2022-11-04 07:39:50 -07:00
wobsoriano
7490dbd090 chore: bump to 0.4.0-beta.15 2022-11-04 07:37:49 -07:00
wobsoriano
1dc0ca7808 feat: remove custom async data error handler 2022-11-04 07:37:08 -07:00
wobsoriano
ffb692a168 feat(deps): bump nuxt to 3.0.0-rc.13 2022-11-04 07:34:47 -07:00
wobsoriano
bb5e02383e remove unused moduile 2022-11-02 08:59:55 -07:00
wobsoriano
c01dddea02 remove local pnpm 2022-11-02 08:56:58 -07:00
wobsoriano
f92aec5b91 feat(deps): bump h3 to 0.8.6 2022-11-02 08:55:52 -07:00
wobsoriano
b6f92d954c update local trpc dependencies to 10.0.0-rc.3 2022-11-02 08:54:58 -07:00
wobsoriano
9108dcbb1d feat: never cache mutation 2022-10-31 22:39:40 -07:00
Robert Soriano
07f415640f bump to 0.4.0-beta.14 2022-10-31 10:09:07 -07:00
Robert Soriano
d5d211d2a8 feat: add subscription types 2022-10-31 10:06:41 -07:00
Robert Soriano
3bfd3585b3 remove unused types 2022-10-31 09:58:51 -07:00
Robert Soriano
8ba8d79114 remove unused generic type 2022-10-31 09:55:00 -07:00
Robert Soriano
b7f72eac0a feat: respect user added abort signal 2022-10-31 09:53:00 -07:00
Robert Soriano
4c8a6d7b35 feat: add abortOnUnmount option 2022-10-31 09:50:15 -07:00
Robert Soriano
76619ac541 bump to 0.4.0-beta.13 2022-10-31 01:01:44 -07:00
Robert Soriano
d5384cef02 add .vercel to ignored 2022-10-31 00:50:10 -07:00
Robert Soriano
fb3a0a834e add vercel static output to ignored 2022-10-31 00:43:08 -07:00
Robert Soriano
b58d662edc update docs 2022-10-31 00:37:45 -07:00
Robert Soriano
5d560867bb update http handler params 2022-10-30 22:24:49 -07:00
Robert Soriano
ac7fbf3998 update trpc param error handler 2022-10-30 22:21:08 -07:00
Robert Soriano
45fbab4c5f update docs 2022-10-30 21:40:20 -07:00
Robert Soriano
05ad1f96f6 export server types 2022-10-30 21:31:57 -07:00
Robert Soriano
41e6117113 add zod error formatting for playground 2022-10-30 21:30:07 -07:00
Robert Soriano
81c7608fb0 add batching and other opts 2022-10-30 21:25:37 -07:00
Robert Soriano
a614bc588e allow passing of other client options 2022-10-30 21:13:54 -07:00
Robert Soriano
3b0609e395 allow passing of other client options 2022-10-30 21:09:42 -07:00
Robert Soriano
0819c21b94 eslint fix 2022-10-30 18:19:32 -07:00
Robert Soriano
deea70b743 check trpc path via h3 context params 2022-10-30 18:17:14 -07:00
Robert Soriano
0ee322c8a2 run dev playground with concurrently 2022-10-30 17:35:23 -07:00
Robert Soriano
fc225ced0a bump to beta.12 2022-10-30 17:06:43 -07:00
Robert Soriano
b1d00c1f88 update compat 2022-10-30 17:06:35 -07:00
Robert Soriano
e0a6bb3576 bump to beta.11 2022-10-30 17:03:59 -07:00
Robert Soriano
fcd6cb1546 fix module compat 2022-10-30 17:03:49 -07:00
Robert Soriano
471ac30040 bump to beta.10 2022-10-30 17:00:27 -07:00
Robert Soriano
a7013db712 add module helper 2022-10-30 17:00:19 -07:00
Robert Soriano
bdcff360f0 bump to beta.9 2022-10-30 14:24:50 -07:00
Robert Soriano
1490790edc import composables from nuxt/app 2022-10-30 14:24:39 -07:00
Robert Soriano
4c5e6812b1 bump to beta.8 2022-10-30 13:36:28 -07:00
Robert Soriano
23d2954aae import composables from #imports 2022-10-30 13:35:58 -07:00
Robert Soriano
3c39746fa2 bump to beta.7 2022-10-30 13:31:04 -07:00
Robert Soriano
5fb7a9d444 import composables from nuxt/app 2022-10-30 13:30:57 -07:00
Robert Soriano
a2275567e8 bump to beta.6 2022-10-30 13:25:11 -07:00
Robert Soriano
c040db4308 update esm extensions to mjs 2022-10-30 13:24:39 -07:00
Robert Soriano
72755315a5 bump to beta.5 2022-10-30 13:09:58 -07:00
Robert Soriano
3d49810641 switch package type to module 2022-10-30 13:09:45 -07:00
Robert Soriano
ebb6637238 chore: release v0.4.0-beta.4 2022-10-30 13:02:11 -07:00
Robert Soriano
0c83ade918 update release script 2022-10-30 13:02:01 -07:00
Robert Soriano
97d9b3ddf8 import useAsyncData and useState from #app 2022-10-30 12:57:57 -07:00
Robert Soriano
eaa61f3700 bump local pnpm to 7.14.1 2022-10-30 12:35:30 -07:00
Robert Soriano
a5a9f430d5 chore: release v0.4.0-beta.3 2022-10-30 12:34:40 -07:00
Robert Soriano
94f59c396a update local deps 2022-10-30 12:30:03 -07:00
Robert Soriano
fe4c10ae25 remove outDir 2022-10-30 12:29:01 -07:00
Robert Soriano
7a97127353 update directory structure 2022-10-30 12:25:28 -07:00
Robert Soriano
0c4a43ec19 update playground 2022-10-30 12:19:57 -07:00
Robert Soriano
da6d738406 update docs 2022-10-30 12:00:21 -07:00
Robert Soriano
70c0b35c48 complete usage docs 2022-10-30 11:18:57 -07:00
Robert Soriano
da52b350ba add initial docs 2022-10-30 11:00:09 -07:00
Robert Soriano
b45c613ce7 immediately execute mutations 2022-10-30 01:52:20 -07:00
Robert Soriano
025ce7e028 chore: release v0.4.0-beta.2 2022-10-29 23:10:16 -07:00
Robert Soriano
4e637a4ec3 add root tsconfig 2022-10-29 23:09:44 -07:00
Robert Soriano
2b5daf54fa chore: release v0.4.0-beta.1 2022-10-29 22:52:42 -07:00
Robert Soriano
217c3fd8e9 chore: release v0.4.0-beta.0 2022-10-29 22:51:51 -07:00
Robert Soriano
14617d864d fix build 2022-10-29 22:48:13 -07:00
Robert Soriano
3522cc9327 move to pnpm workspace 2022-10-29 22:45:57 -07:00
Robert Soriano
09500bc868 update readme 2022-10-29 22:39:43 -07:00
Robert Soriano
6ddfbf9629 update playground 2022-10-29 22:38:10 -07:00
Robert Soriano
97dfefe6cd add superjson to playground 2022-10-29 22:33:59 -07:00
Robert Soriano
b61dbd1786 update playground 2022-10-29 22:29:21 -07:00
Robert Soriano
4bb2cdc2d2 save trpc server error to client 2022-10-29 22:16:06 -07:00
Robert Soriano
29909328c4 update playground 2022-10-29 21:58:00 -07:00
Robert Soriano
da44e5429f update devDeps 2022-10-29 21:51:27 -07:00
Robert Soriano
ae920a37fd make trpc-nuxt flexible by exporting server and client functions instead of nuxt modules 2022-10-29 21:50:27 -07:00
Robert Soriano
bad2de134b remove queryKey for mutation 2022-10-29 19:38:29 -07:00
Robert Soriano
14da8bbda6 update example 2022-10-29 19:31:42 -07:00
Robert Soriano
e69dacf07a import useAsyncData from #app 2022-10-29 19:26:32 -07:00
Robert Soriano
f8de361eaf add inspiration comment 2022-10-29 19:24:43 -07:00
Robert Soriano
0a5735125f move types to another file 2022-10-29 19:23:54 -07:00
Robert Soriano
a04ed72c45 update playground 2022-10-29 19:20:47 -07:00
Robert Soriano
d435c7c9c5 update export paths 2022-10-29 19:18:15 -07:00
Robert Soriano
a3ce2c9a92 remove unused module imports 2022-10-29 19:15:59 -07:00
Robert Soriano
b0e7cc1854 rename api to server 2022-10-29 19:12:44 -07:00
Robert Soriano
9d07dab57d add custom fetch function for node 18 2022-10-29 19:12:01 -07:00
Robert Soriano
7257842438 rewrite client 2022-10-29 19:02:14 -07:00
Robert Soriano
b72b0449c1 update client types 2022-10-28 15:50:54 -07:00
Robert Soriano
d47fbc4537 add exported router type to plugin 2022-10-28 15:40:31 -07:00
Robert Soriano
c0b570ca09 api support for trpc v10 2022-10-28 15:39:31 -07:00
Robert Soriano
95f5478cdc update server routes in playground 2022-10-28 15:27:50 -07:00
Robert Soriano
000ac03295 update client to use v10 client 2022-10-28 15:13:29 -07:00
Robert Soriano
91421c1849 move @trpc/client and @trpc/server to peerDependencies 2022-10-28 15:06:13 -07:00
49 changed files with 4282 additions and 2408 deletions

153
README.md
View File

@@ -1,12 +1,10 @@
# tRPC-Nuxt
[![Version](https://img.shields.io/npm/v/trpc-nuxt?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/trpc-nuxt)
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
<p align="center">
<figure>
<img src="https://i.imgur.com/AjmNUxj.gif" alt="Demo" />
<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.
@@ -15,154 +13,9 @@ End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
</figure>
</p>
## Install
Docs: https://trpc-nuxt.vercel.app
```bash
npm i trpc-nuxt
```
```ts
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
modules: ['trpc-nuxt'],
trpc: {
baseURL: '', // Set empty string (default) to make requests by relative address
endpoint: '/trpc', // defaults to /trpc
installPlugin: true, // defaults to true. Add @trpc/client plugin and composables
},
typescript: {
strict: true // required to make input/output types work
}
})
```
## Usage
Expose your tRPC [routes](https://trpc.io/docs/router) under `~/server/trpc/index.ts`:
```ts
// ~/server/trpc/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import * as trpc from '@trpc/server'
import { z } from 'zod' // yup/superstruct/zod/myzod/custom
export const router = trpc.router()
// queries and mutations...
.query('getUsers', {
async resolve(req) {
// use your ORM of choice
return await UserModel.all()
},
})
.mutation('createUser', {
// validate input with Zod
input: z.object({ name: z.string().min(5) }),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
Use the client like so:
```ts
const client = useClient() // auto-imported
const users = await client.query('getUsers')
const newUser = await client.mutation('createUser', {
name: 'wagmi'
})
```
## useAsyncQuery
A thin wrapper around [`useAsyncData`](https://v3.nuxtjs.org/api/composables/use-async-data/) and `client.query()`.
The first argument is a `[path, input]`-tuple - if the `input` is optional, you can omit the, `input`-part.
You'll notice that you get autocompletion on the `path` and automatic typesafety on the `input`.
```ts
const {
data,
pending,
error,
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
lazy: false
})
```
## useClientHeaders
A composable that lets you add additional properties to pass to the tRPC Client. It uses `useState` from [nuxt 3](https://v3.nuxtjs.org/api/composables/use-state).
```ts
const headers = useClientHeaders()
const { data: token } = await useAsyncQuery(['auth.login', { username, password }])
headers.value.Authorization = `Bearer ${token}`
// All client calls will now include the Authorization header.
```
If your app needs transformers and other option that needs to be passed in `@trpc/client`, you will need to opt-out of the plugin by setting `installPlugin` to false in your `nuxt.config.ts` file and create your own. You can just copy the contents of the client plugin [here](https://github.com/wobsoriano/trpc-nuxt/blob/master/src/runtime/client.ts).
## Options
trpc-nuxt accepts the following options exposed under `~/server/trpc/index.ts`:
```ts
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import type { H3Event } from 'h3'
import type { OnErrorPayload } from 'trpc-nuxt/api'
export const router = trpc.router<inferAsyncReturnType<typeof createContext>>()
// Optional
// https://trpc.io/docs/context
export const createContext = (event: H3Event) => {
// ...
return {
/** context data */
}
}
// Optional
// https://trpc.io/docs/caching#using-responsemeta--to-cache-responses
export const responseMeta = () => {
// ...
return {
// { headers: ... }
}
}
// Optional
// https://trpc.io/docs/error-handling#handling-errors
export const onError = (payload: OnErrorPayload<typeof router>) => {
// Do whatever here like send to bug reporting and stuff
}
```
## Recipes
- [Validation](/recipes/validation.md)
- [Authorization](/recipes/authorization.md)
- [Merging Routers](/recipes/merging-routers.md)
- [Error Handling](/recipes/error-handling.md)
- [Error Formatting](/recipes/error-formatting.md)
- [Inference Helpers](/recipes/inference-helpers.md)
Learn more about tRPC.io [here](https://trpc.io/docs/v9).
For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3).
## Recommended IDE Setup

1
api.d.ts vendored
View File

@@ -1 +0,0 @@
export * from './dist/runtime/api'

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.vercel

1
docs/README.md Normal file
View File

@@ -0,0 +1 @@
docs

25
docs/app.config.ts Normal file
View File

@@ -0,0 +1,25 @@
export default defineAppConfig({
docus: {
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

@@ -0,0 +1,38 @@
---
title: Installation
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Installation
::code-group
```bash [pnpm]
pnpm add @trpc/server@next @trpc/client@next trpc-nuxt zod
```
```bash [npm]
npm install @trpc/server@next @trpc/client@next trpc-nuxt zod
```
```bash [yarn]
yarn add @trpc/server@next @trpc/client@next trpc-nuxt zod
```
::
#### Why @trpc/server?
For implementing tRPC endpoints and routers.
#### Why @trpc/client?
For making typesafe API calls from your client.
#### Why zod?
Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required.
## Next Steps
Now that you've installed the required dependencies, you are ready to start building your application.

View File

@@ -0,0 +1,130 @@
---
title: Usage
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Usage
## Recommended file structure
Recommended but not enforced file structure. This is what you get when starting from [the examples](../main/example-apps.md).
```graphql
.
server
api
trpc
[trpc].ts # <-- tRPC HTTP handler
[..]
trpc
routers
index.ts # <-- main app router
todo.ts # <-- sub routers
[..]
context.ts # <-- create app context
trpc.ts # <-- procedure helpers
plugins
client.ts # <-- tRPC Client as a plugin
[..]
```
## 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]
import { initTRPC } from '@trpc/server'
// Avoid exporting the entire t-object since it's not very
// 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
export const publicProcedure = t.procedure
```
```ts [server/trpc/routers/index.ts]
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
export const appRouter = router({
hello: publicProcedure
.input(
z.object({
text: z.string().nullish(),
}),
)
.query(({ input }) => {
return {
greeting: `hello ${input?.text ?? 'world'}`,
}
}),
})
// export type definition of API
export type AppRouter = typeof appRouter
```
```ts [server/api/trpc/[trpc].ts]
import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '@/server/trpc/routers'
// export API handler
export default createNuxtApiHandler({
router: appRouter,
createContext: () => ({}),
})
```
::
::alert{type=info}
If you need to split your router into several subrouters, you can implement them in the `server/trpc/routers` directory and import and [merge them](https://trpc.io/docs/v10/merging-routers) to a single root `appRouter`.
::
## 2. Create tRPC client plugin
Create a strongly-typed plugin using your API's type signature.
```ts [plugins/client.ts]
import { httpBatchLink, createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '@/server/trpc/routers'
export default defineNuxtPlugin(() => {
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
/**
* 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',
}),
],
})
return {
provide: {
client,
},
}
})
```
## 3. Make API requests
```vue [pages/index.vue]
<script setup lang="ts">
const { $client } = useNuxtApp()
const data = await $client.hello.query({ text: 'client' })
</script>
<template>
<p>{{data?.greeting }}</p>
</template>
```

View File

@@ -0,0 +1,40 @@
---
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.
## 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,40 @@
---
title: Headers
---
# Headers
We can use the built-in [useRequestHeaders](https://v3.nuxtjs.org/api/composables/use-request-headers/) to set outgoing request headers:
```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

@@ -0,0 +1,6 @@
---
title: Basic
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Basic Example

View File

@@ -0,0 +1,6 @@
---
title: Multiple Routers
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Multi Routers

31
docs/content/index.md Normal file
View File

@@ -0,0 +1,31 @@
---
title: "tRPC Nuxt"
description: "End-to-end typesafe APIs in Nuxt applications."
navigation: false
layout: page
---
::block-hero
---
cta:
- Get Started
- /get-started/installation
secondary:
- Star on GitHub ->
- https://github.com/wobsoriano/trpc-nuxt
snippet: npm install trpc-nuxt
---
#title
tRPC [Nuxt]{.text-primary-500}
#description
End-to-end typesafe APIs in Nuxt applications.
#extra
::list
- Automatic typesafety
- Snappy DX
- Autocompletion on the client, for inputs, outputs and errors
::
::

22
docs/nuxt.config.ts Normal file
View File

@@ -0,0 +1,22 @@
export default defineNuxtConfig({
app: {
pageTransition: false,
layoutTransition: false
},
modules: ['@nuxtlabs/github-module'],
extends: process.env.DOCUS_THEME_PATH || '@nuxt-themes/docus',
github: {
owner: 'wobsoriano',
repo: 'trpc-nuxt',
branch: 'next'
},
colorMode: {
preference: 'dark'
},
build: {
transpile: [/content-edge/, /github-module/]
},
nitro: {
preset: 'vercel'
}
})

17
docs/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "docs",
"description": "Docs for TRPC-Nuxt",
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi build",
"preview": "nuxi preview"
},
"dependencies": {
"nuxt": "^3.0.0"
},
"devDependencies": {
"@nuxt-themes/docus": "^0.3.1",
"@nuxtlabs/github-module": "^1.5.3"
}
}

18
docs/tokens.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineTheme } from 'pinceau'
export default defineTheme({
colors: {
primary: {
50: '#BFEDFC',
100: '#B0E9FB',
200: '#93DEFA',
300: '#76D4F9',
400: '#58C8F7',
500: '#3BBBF6',
600: '#0BA6F3',
700: '#0981C2',
800: '#075E91',
900: '#043D61'
}
}
})

3
docs/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -1,64 +1,68 @@
{
"name": "trpc-nuxt",
"description": "End-to-end typesafe APIs in Nuxt applications.",
"type": "module",
"version": "0.3.3",
"version": "0.4.3",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": {
"require": "./dist/module.cjs",
"import": "./dist/module.mjs"
},
"./api": {
"types": "./dist/runtime/api.d.ts",
"import": "./dist/runtime/api.mjs"
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist",
"*.d.ts"
"dist"
],
"scripts": {
"dev": "concurrently \"pnpm build -- --watch\" \"pnpm --filter playground dev\"",
"dev:prepare": "pnpm build && nuxt prepare playground",
"prepublishOnly": "pnpm build",
"build": "nuxt-module-build",
"play": "pnpm build && nuxi dev playground",
"build:playground": "nuxi build playground",
"build": "tsup",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag && npm publish",
"prepare": "nuxi prepare playground"
"release": "bumpp && npm publish"
},
"peerDependencies": {
"@trpc/client": "<10.0.0",
"@trpc/server": "<10.0.0"
"@trpc/client": "^10.0.0",
"@trpc/server": "^10.0.0"
},
"dependencies": {
"@nuxt/kit": "3.0.0-rc.13",
"@trpc/client": "^9.27.4",
"@trpc/server": "^9.27.4",
"dedent": "^0.7.0",
"defu": "^6.1.0",
"h3": "^0.8.6",
"ohash": "^0.1.5",
"pathe": "^0.3.9",
"ufo": "^0.8.6"
"h3": "^1.0.1",
"nanoid": "^4.0.0",
"ohash": "^1.0.0",
"ufo": "^1.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^0.23.1",
"@nuxt/module-builder": "^0.2.0",
"@types/dedent": "^0.7.0",
"bumpp": "^7.2.0",
"@nuxtjs/eslint-config-typescript": "^11.0.0",
"@trpc/client": "^10.1.0",
"@trpc/server": "^10.1.0",
"bumpp": "^8.2.1",
"concurrently": "^7.5.0",
"eslint": "^8.25.0",
"nuxt": "3.0.0-rc.13",
"trpc-nuxt": "workspace:*",
"zod": "^3.19.1"
"tsup": "6.4.0",
"typescript": "^4.7.4"
},
"eslintConfig": {
"extends": "@antfu",
"extends": [
"@nuxtjs/eslint-config-typescript"
],
"rules": {
"no-console": "warn"
"@typescript-eslint/no-unused-vars": [
"off"
],
"vue/multi-word-component-names": "off",
"vue/no-multiple-template-root": "off"
}
}
},
"eslintIgnore": [
"*.json",
"node_modules",
"*.md",
"dist",
".output"
]
}

View File

@@ -1,9 +1,3 @@
import Module from '../src/module'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [Module],
runtimeConfig: {
baseURL: '',
},
})

View File

@@ -1,4 +1,21 @@
{
"name": "playground",
"private": true
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@trpc/client": "^10.1.0",
"@trpc/server": "^10.1.0",
"superjson": "^1.11.0",
"trpc-nuxt": "workspace:*",
"zod": "^3.19.1"
},
"devDependencies": {
"nuxt": "^3.0.0"
}
}

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>

View File

@@ -1,55 +1,55 @@
<script setup lang="ts">
const client = useClient()
const headers = useClientHeaders()
const { data: todos, pending, error, refresh } = await useAsyncQuery(['getTodos'])
import { TRPCClientError } from '@trpc/client';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '~~/server/trpc/routers';
const addHeader = () => {
headers.value.authorization = 'Bearer abcdefghijklmnop'
console.log(headers.value)
}
const { $client } = useNuxtApp()
const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7)
try {
const result = await client.mutation('addTodo', {
const x = await $client.todo.addTodo.mutate({
id: Date.now(),
userId: 69,
title,
completed: false,
completed: false
})
console.log('Todo: ', result)
}
catch (e) {
console.log(x)
} catch (e) {
console.log(e)
}
}
type RouterOutput = inferRouterOutputs<AppRouter>;
type ErrorOutput = TRPCClientError<AppRouter>
const { data: todos, pending, error, refresh } = await useAsyncData<RouterOutput['todo']['getTodos'], ErrorOutput>(() => $client.todo.getTodos.query())
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else-if="todos">
<ul>
<li v-for="t in todos.slice(0, 10)" :key="t.id">
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`">
Title: {{ t.title }}
</NuxtLink>
</li>
</ul>
<button @click="addTodo">
Add Todo
</button>
<button @click="() => refresh()">
Refresh
</button>
<button @click="addHeader">
Add header
</button>
<div>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else>
<ul>
<li v-for="t in todos?.slice(0, 10)" :key="t.id">
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`">
Title: {{ t.title }}
</NuxtLink>
</li>
</ul>
<button @click="addTodo">
Add Todo
</button>
<button @click="() => refresh()">
Refresh
</button>
</div>
</div>
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
const route = useRoute()
const { data: todo, pending, error } = await useAsyncQuery(['getTodo', Number(route.params.id)])
const { $client } = useNuxtApp()
const { data: todo, pending, error } = await useAsyncData(() => $client.todo.getTodo.query(Number(route.params.id)))
</script>
<template>
@@ -11,8 +12,8 @@ const { data: todo, pending, error } = await useAsyncQuery(['getTodo', Number(ro
{{ error.data.code }}
</div>
<div v-else>
ID: {{ todo.id }} <br>
Title: {{ todo.title }} <br>
Completed: {{ todo.completed }}
ID: {{ todo?.id }} <br>
Title: {{ todo?.title }} <br>
Completed: {{ todo?.completed }}
</div>
</template>

View File

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

View File

@@ -0,0 +1,23 @@
import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '@/server/trpc/routers'
import { createContext } from '@/server/trpc/context'
export default createNuxtApiHandler({
router: appRouter,
/**
* @link https://trpc.io/docs/context
*/
createContext,
onError ({ error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
console.error('Something went wrong', error)
}
}
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
// responseMeta() {
// // ...
// },
})

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { inferAsyncReturnType } from '@trpc/server'
import type { H3Event } from 'h3'
export type Context = inferAsyncReturnType<typeof createContext>
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export function createContext (
event: H3Event
) {
// for API-response caching see https://trpc.io/docs/caching
console.log('cookies', parseCookies(event))
return {}
}

View File

@@ -1,53 +0,0 @@
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import { z } from 'zod'
import type { H3Event } from 'h3'
const baseURL = 'https://jsonplaceholder.typicode.com'
const TodoShape = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean(),
})
export type Todo = z.infer<typeof TodoShape>
export const router = trpc.router<Context>()
.query('getTodos', {
async resolve() {
return await $fetch<Todo[]>(`${baseURL}/todos`)
},
})
.query('getTodo', {
input: z.number(),
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos/${req.input}`)
},
})
.mutation('addTodo', {
input: TodoShape,
async resolve(req) {
console.log(req.input)
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})
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'd might want to do in your ctx fn
// const x = useCookies(event)
console.log(event.req.headers)
return {
}
}
type Context = inferAsyncReturnType<typeof createContext>

View File

@@ -0,0 +1,10 @@
import { router } from '../trpc'
import { todoRouter } from './todo'
import { userRouter } from './user'
export const appRouter = router({
todo: todoRouter,
user: userRouter
})
export type AppRouter = typeof appRouter

View File

@@ -0,0 +1,33 @@
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
const baseURL = 'https://jsonplaceholder.typicode.com'
const TodoShape = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean()
})
export type Todo = z.infer<typeof TodoShape>
export const todoRouter = router({
getTodos: publicProcedure
.query(() => {
return $fetch<Todo[]>(`${baseURL}/todos`)
}),
getTodo: publicProcedure
.input(z.number())
.query((req) => {
return $fetch<Todo>(`${baseURL}/todos/${req.input}`)
}),
addTodo: publicProcedure
.input(TodoShape)
.mutation((req) => {
return $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input
})
})
})

View File

@@ -0,0 +1,33 @@
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
const baseURL = 'https://jsonplaceholder.typicode.com'
const UserShape = z.object({
id: z.number(),
name: z.string(),
username: z.string(),
email: z.string()
})
export type User = z.infer<typeof UserShape>
export const userRouter = router({
getUsers: publicProcedure
.query(() => {
return $fetch<User[]>(`${baseURL}/users`)
}),
getUser: publicProcedure
.input(z.number())
.query((req) => {
return $fetch<User>(`${baseURL}/users/${req.input}`)
}),
addUser: publicProcedure
.input(UserShape)
.mutation((req) => {
return $fetch<User>(`${baseURL}/users`, {
method: 'POST',
body: req.input
})
})
})

View File

@@ -0,0 +1,43 @@
import { initTRPC } from '@trpc/server'
import superjson from 'superjson'
import { ZodError } from 'zod'
import type { Context } from './context'
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter ({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST' &&
error.cause instanceof ZodError
? error.cause!.flatten()
: null
}
}
}
})
/**
* Create a router
* @see https://trpc.io/docs/v10/router
*/
export const router = t.router
/**
* Create an unprotected procedure
* @see https://trpc.io/docs/v10/procedures
**/
export const publicProcedure = t.procedure
/**
* @see https://trpc.io/docs/v10/middlewares
*/
export const middleware = t.middleware
/**
* @see https://trpc.io/docs/v10/merging-routers
*/
export const mergeRouters = t.mergeRouters

4
playground/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

4851
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,3 @@
packages:
- playground
- docs

View File

@@ -1,102 +0,0 @@
## 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.
### Create context from request headers
```ts
// ~/server/trpc/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import type { H3Event } from 'h3'
import { decodeAndVerifyJwtToken } from '~/somewhere/in/your/app/utils'
// The app's context - is generated for each incoming request
export async function createContext({ req }: 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'd might want to do in your ctx fn
async function getUserFromHeader() {
if (req.headers.authorization) {
const user = await decodeAndVerifyJwtToken(req.headers.authorization.split(' ')[1])
return user
}
return null
}
const user = await getUserFromHeader()
return {
user,
}
}
type Context = inferAsyncReturnType<typeof createContext>
// [..] Define API handler and app router
```
### Option 1: Authorize using resolver
```ts
import { TRPCError } from '@trpc/server'
export const router = trpc
.router<Context>()
// open for anyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
// checked in resolver
.query('secret', {
resolve: ({ ctx }) => {
if (!ctx.user)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return {
secret: 'sauce',
}
},
})
```
### Option 2: Authorize using middleware
```ts
import * as trpc from '@trpc/server'
import { TRPCError } from '@trpc/server'
// Merging routers: https://trpc.io/docs/merging-routers
export const router = trpc
.router<Context>()
// this is accessible for everyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
.merge(
'admin.',
trpc.router<Context>()
// this protects all procedures defined next in this router
.middleware(async ({ ctx, next }) => {
if (!ctx.user?.isAdmin)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return next()
})
.query('secret', {
resolve: ({ ctx }) => {
return {
secret: 'sauce',
}
},
}),
)
```
Learn more about authorization [here](https://trpc.io/docs/authorization).

View File

@@ -1,41 +0,0 @@
## Error Formatting
The error formatting in your router will be inferred all the way to your client (& Vue components).
### Adding custom formatting
```ts
// ~/server/trpc/index.ts
import * as trpc from '@trpc/server'
export const router = trpc.router<Context>()
.formatError(({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST'
&& error.cause instanceof ZodError
? error.cause.flatten()
: null,
}
}
})
```
### Usage in Vue
```html
<script setup lang="ts">
const { error } = await useAsyncQuery(['getUser', { id: 69 }])
</script>
<template>
<pre v-if="error?.data?.zodError">
{{ JSON.stringify(error.data.zodError, null, 2) }}
</pre>
</template>
```
Learn more about error formatting [here](https://trpc.io/docs/error-formatting).

View File

@@ -1,15 +0,0 @@
## Handling errors
All errors that occur in a procedure go through the `onError` method before being sent to the client. Here you can handle or change errors.
```ts
// ~/server/trpc/index.ts
import * as trpc from '@trpc/server'
export function onError({ error, type, path, input, ctx, req }) {
console.error('Error:', error)
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
}
}
```

View File

@@ -1,80 +0,0 @@
## Inference Helpers
`@trpc/server` exports the following helper types to assist with inferring these types from the `router` exported in `~/server/trpc/index.ts`:
- `inferProcedureOutput<TProcedure>`
- `inferProcedureInput<TProcedure>`
- `inferSubscriptionOutput<TRouter, TPath>`
```ts
// ~/utils/trpc.ts
import type { router } from '~/server/trpc/index.ts'
type AppRouter = typeof router
/**
* Enum containing all api query paths
*/
export type TQuery = keyof AppRouter['_def']['queries']
/**
* Enum containing all api mutation paths
*/
export type TMutation = keyof AppRouter['_def']['mutations']
/**
* Enum containing all api subscription paths
*/
export type TSubscription = keyof AppRouter['_def']['subscriptions']
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = InferQueryOutput<'hello'>
*/
export type InferQueryOutput<TRouteKey extends TQuery> = inferProcedureOutput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the input of a query resolver
* @example type HelloInput = InferQueryInput<'hello'>
*/
export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the output of a mutation resolver
* @example type HelloOutput = InferMutationOutput<'hello'>
*/
export type InferMutationOutput<TRouteKey extends TMutation> =
inferProcedureOutput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the input of a mutation resolver
* @example type HelloInput = InferMutationInput<'hello'>
*/
export type InferMutationInput<TRouteKey extends TMutation> =
inferProcedureInput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the output of a subscription resolver
* @example type HelloOutput = InferSubscriptionOutput<'hello'>
*/
export type InferSubscriptionOutput<TRouteKey extends TSubscription> =
inferProcedureOutput<AppRouter['_def']['subscriptions'][TRouteKey]>
/**
* This is a helper method to infer the asynchronous output of a subscription resolver
* @example type HelloAsyncOutput = InferAsyncSubscriptionOutput<'hello'>
*/
export type InferAsyncSubscriptionOutput<TRouteKey extends TSubscription> =
inferSubscriptionOutput<AppRouter, TRouteKey>
/**
* This is a helper method to infer the input of a subscription resolver
* @example type HelloInput = InferSubscriptionInput<'hello'>
*/
export type InferSubscriptionInput<TRouteKey extends TSubscription> =
inferProcedureInput<AppRouter['_def']['subscriptions'][TRouteKey]>
```

View File

@@ -1,46 +0,0 @@
# Merging Routers
Writing all API-code in your code in the same file is not a great idea. It's easy to merge routers with other routers.
Define your routes:
```ts
// ~/server/trpc/routes/posts.ts
export const posts = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/routes/users.ts
export const users = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/index.ts
import { users } from './routes/users'
import { posts } from './routes/posts'
export const router = trpc.router()
.merge('user.', users) // prefix user procedures with "user."
.merge('post.', posts) // prefix post procedures with "post."
```
and use it like this:
```html
<script setup lang="ts">
const { data: users } = await useAsyncQuery(['user.list'])
const { data: posts } = await useAsyncQuery(['post.list'])
</script>
```

View File

@@ -1,49 +0,0 @@
## Validation
tRPC works out-of-the-box with yup/superstruct/zod/myzod/custom validators.
### Input Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.mutation('createUser', {
// validate input with Zod
input: z.object({
name: z.string().min(5)
}),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
### Output Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.query('hello', {
// validate output with Zod
output: z.object({
greeting: z.string()
}),
// expects return type of { greeting: string }
resolve() {
return {
greeting: 'hello!',
}
},
})
```
Learn more about input validation [here](https://trpc.io/docs/router#input-validation).

View File

@@ -1,16 +1,18 @@
import { resolveHTTPResponse } from '@trpc/server'
import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
ResponseMeta,
TRPCError,
inferRouterContext,
inferRouterError,
inferRouterError
} from '@trpc/server'
import {
TRPCError
} from '@trpc/server'
import { createURL } from 'ufo'
import type { H3Event } from 'h3'
import { defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/dist/declarations/src/rpc'
import { createError, defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
@@ -30,51 +32,85 @@ export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: H3Event['req']
req: H3Event['node']['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export function createTRPCHandler<Router extends AnyRouter>({
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,
endpoint,
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
endpoint: string
}) {
batching
}: ResolveHTTPRequestOptions<TRouter>) {
return defineEventHandler(async (event) => {
const {
req,
res,
} = event
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,
query: $url.searchParams
},
path: $url.pathname.substring(endpoint.length + 1),
createContext: async () => createContext?.(event),
path,
createContext: async () => await createContext?.(event),
responseMeta,
onError: (o) => {
onError?.({
...o,
req,
req
})
},
}
})
const { status, headers, body } = httpResponse

View File

@@ -1,80 +0,0 @@
import { fileURLToPath } from 'url'
import { join, resolve } from 'pathe'
import { defu } from 'defu'
import dedent from 'dedent'
import { addImports, addPlugin, addServerHandler, addTemplate, defineNuxtModule, useLogger } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
endpoint: string
installPlugin?: boolean
}
const metaName = 'trpc-nuxt'
export default defineNuxtModule<ModuleOptions>({
meta: {
name: metaName,
configKey: 'trpc',
},
defaults: {
baseURL: '',
endpoint: '/trpc',
installPlugin: true,
},
async setup(options, nuxt) {
const logger = useLogger(metaName)
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-handler')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
const trpcOptionsPath = join(nuxt.options.srcDir, 'server/trpc')
// Final resolved configuration
const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, {
baseURL: options.baseURL,
endpoint: options.endpoint,
installPlugin: options.installPlugin,
})
addServerHandler({
route: `${finalConfig.endpoint}/*`,
handler: handlerPath,
})
addTemplate({
filename: 'trpc-handler.ts',
write: true,
getContents() {
return dedent`
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '${trpcOptionsPath}'
export default createTRPCHandler({
...functions,
endpoint: '${finalConfig.endpoint}'
})
`
},
})
if (finalConfig.installPlugin) {
addImports([
{ name: 'useClient', from: join(runtimeDir, 'client') },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
{ name: 'useClientHeaders', from: join(runtimeDir, 'client') },
{ name: 'getQueryKey', from: join(runtimeDir, 'client') },
])
addPlugin(resolve(runtimeDir, 'plugin'))
logger.success('Plugin successfully installed.')
}
else {
logger.info('Plugin not installed. Create your own @trpc/client client plugin and composables.')
}
},
})

View File

@@ -1,71 +0,0 @@
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server'
import type { TRPCClient, TRPCClientErrorLike } from '@trpc/client'
import { objectHash } from 'ohash'
import type { Ref } from 'vue'
import { useAsyncData, useNuxtApp, useState } from '#app'
import type { router } from '~/server/trpc'
type MaybeRef<T> = T | Ref<T>
type AppRouter = typeof router
export type inferProcedures<
TObj extends ProcedureRecord<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>
output: inferProcedureOutput<TObj[TPath]>
};
}
export type TQueries = AppRouter['_def']['queries']
export type TError = TRPCClientErrorLike<AppRouter>
export type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
/**
* Calculates the key used for `useAsyncData` call
* @param pathAndInput
*/
export function getQueryKey<
TPath extends keyof TQueryValues & string,
>(pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>]) {
return `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
}
export async function useAsyncQuery<
TPath extends keyof TQueryValues & string,
TOutput extends TQueryValues[TPath]['output'] = TQueryValues[TPath]['output'],
Transform extends _Transform<TOutput> = _Transform<TOutput, TOutput>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
options: AsyncDataOptions<TOutput, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TError>> {
const { $client } = useNuxtApp()
const key = getQueryKey(pathAndInput)
const result = await useAsyncData(
key,
() => $client.query(...pathAndInput),
// @ts-expect-error: Internal
options,
)
return result as any
}
export function useClient(): TRPCClient<AppRouter> {
const { $client } = useNuxtApp()
return $client
}
export function useClientHeaders(initialValue: MaybeRef<Record<string, any>> = {}): Ref<Record<string, any>> {
return useState('trpc-nuxt-header', () => initialValue)
}

View File

@@ -1,45 +0,0 @@
import * as trpc from '@trpc/client'
import { unref } from 'vue'
import { FetchError } from 'ohmyfetch'
import { useClientHeaders } from './client'
import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#app'
import type { router } from '~/server/trpc'
declare type AppRouter = typeof router
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.trpc
const headers = useRequestHeaders()
const otherHeaders = useClientHeaders()
const baseURL = process.server ? '' : config.baseURL
const client = trpc.createTRPCClient<AppRouter>({
url: `${baseURL}${config.endpoint}`,
headers: () => {
return {
...unref(otherHeaders),
...headers,
}
},
fetch: (input, options) =>
globalThis.$fetch.raw(input.toString(), options)
.catch((e) => {
if (e instanceof FetchError && e.response)
return e.response
throw e
})
.then(response => ({
...response,
json: () => Promise.resolve(response._data),
})),
})
nuxtApp.provide('client', client)
})
declare module '#app' {
interface NuxtApp {
$client: trpc.TRPCClient<AppRouter>
}
}

View File

@@ -1,3 +1,16 @@
{
"extends": "./playground/.nuxt/tsconfig.json"
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"noEmit": true,
"resolveJsonModule": true,
"skipDefaultLibCheck": true
}
}

15
tsup.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
splitting: false,
clean: true,
external: ['#app', '#imports'],
dts: true,
outExtension ({ format }) {
return {
js: format === 'esm' ? '.mjs' : `.${format}`
}
}
})