Compare commits

...

186 Commits

Author SHA1 Message Date
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
Robert Soriano
bdeada0e6e Merge pull request #32 from littlejon85/9-server-error-discard
fix: don't discard error in client
2022-10-28 14:53:09 -07:00
Robert Soriano
fd9b86fb18 release v0.3.1 2022-10-21 12:27:16 -07:00
Robert Soriano
5fab0e54b0 update local deps 2022-10-21 12:24:18 -07:00
Robert Soriano
7d2f464699 Merge pull request #34 from mahdiboomeri/chore
chore: bump nuxt to RC 12
2022-10-21 08:10:53 -07:00
Mahdi Boomeri
fdb9c2b474 refactor: playground nuxt.config 2022-10-21 17:00:22 +03:30
Mahdi Boomeri
4ed504e1e9 fix: h3 CompatibilityEvent 2022-10-21 16:56:09 +03:30
Mahdi Boomeri
92747711c6 chore: bump other deps 2022-10-21 16:53:53 +03:30
Mahdi Boomeri
3646e8cdfe chore: nump nuxt 2022-10-21 16:43:22 +03:30
Jan Dlouhý
88214185a8 fix: use useAsyncData error in client 2022-10-15 22:36:28 +02:00
Jan Dlouhý
ad03ae7b65 fix: don't discard error in client 2022-10-11 14:35:21 +02:00
Robert Soriano
09e8ab536a feat: replace useBody with readBody 2022-10-01 09:15:36 -07:00
Robert Soriano
f7dadd482d Update README.md 2022-09-29 10:20:05 -07:00
Robert Soriano
7b69cff1ed Merge pull request #30 from cawa-93/patch-1
docs: Fix `baseURL` in code sample
2022-09-27 10:18:15 -07:00
Alex Kozack
f2d7763fa4 docs: Fix baseURL in code sample 2022-09-27 10:41:38 +03:00
Robert Soriano
20f5a08cbd update readme 2022-09-24 08:26:36 -07:00
Robert Soriano
75980c0953 release v0.3.0 2022-09-24 08:25:33 -07:00
Robert Soriano
a6f61d404f feat(deps): bump h3 to 0.7.21 2022-09-24 08:24:57 -07:00
Robert Soriano
e53d1be61e feat(deps): bump @nuxt/kit to 3.0.0-rc-11 2022-09-24 08:23:29 -07:00
Robert Soriano
d152ebc7be chore: remove volta config 2022-09-24 08:20:24 -07:00
Robert Soriano
f6daa2b7c3 Merge pull request #28 from robinWongM/feature/direct-api-call-during-ssr
feat: use globalThis.$fetch to call API handler directly on server-side
2022-09-24 08:19:16 -07:00
Robin Wong
7962c7f61e fix: return original FetchResponse when ohmyfetch throws error 2022-09-18 09:46:59 +00:00
Robin Wong
453845fb14 fix: eslint errors 2022-09-18 08:44:43 +00:00
Robin Wong
ee3ee485ba feat: use globalThis.$fetch to call API handler directly on server-side 2022-09-16 16:53:15 +00:00
Robert Soriano
cdf29bee67 release v0.2.8 2022-09-08 09:36:32 -07:00
Robert Soriano
d3d35404b1 update release script 2022-09-08 09:36:24 -07:00
Robert Soriano
e57c919157 Merge pull request #26 from elfpie/feature/update-nuxt
Bump nuxt to RC9
2022-09-08 08:11:24 -07:00
Felipe
d0fffbbc64 feat(deps): bump nuxt to 3.0.0-rc.9 2022-09-07 17:59:23 -03:00
Felipe
433d5ada46 feat(deps): bump @trpc/client to 9.27.2 2022-09-07 17:59:23 -03:00
Felipe
5d180460af feat(deps): bump @trpc/server to 9.27.2 2022-09-07 17:59:23 -03:00
Felipe
8991148d39 feat(deps): bump @nuxt/kit to 3.0.0-rc.9 2022-09-07 17:59:15 -03:00
Robert Soriano
dd05285d65 release v0.2.7 2022-08-31 12:10:12 -07:00
Robert Soriano
d058ff241a update playground 2022-08-31 12:09:45 -07:00
Robert Soriano
fb4894e264 Merge pull request #25 from mahdiboomeri/fix/useClientHeaders
Fix useClientHeaders
2022-08-31 12:07:05 -07:00
Mahdi Boomeri
186faff140 docs: replaced useStorage with useState in useClientHeaders 2022-08-31 19:19:27 +04:30
Mahdi Boomeri
b9cdb228b7 refactor: remove vueuse dependency 2022-08-31 19:17:44 +04:30
Mahdi Boomeri
1e67434a8f fix: useClientHeaders returning an empty object 2022-08-31 19:13:29 +04:30
Robert Soriano
019bb20e50 release v0.2.6 2022-08-20 00:10:00 -07:00
Robert Soriano
695ce4c56e feat(deps): bump @trpc/server from 9.27.0 to 9.27.1 2022-08-20 00:09:39 -07:00
Robert Soriano
38649b7023 feat(deps): bump @trpc/client from 9.27.0 to 9.27.1 2022-08-20 00:09:17 -07:00
Robert Soriano
5cfef32baa release v0.2.5 2022-08-14 12:38:56 -07:00
Robert Soriano
8497289c65 update readme 2022-08-14 12:38:31 -07:00
Robert Soriano
224c4b317b move ohash to deps 2022-08-14 12:29:04 -07:00
Robert Soriano
d5d6780ca8 bump ohash 2022-08-14 12:28:03 -07:00
Robert Soriano
252d0f6692 feat(deps): bump vueuse from 8.9.3 to 9.1.0 2022-08-14 12:27:10 -07:00
Robert Soriano
d022710591 bump dev deps 2022-08-14 12:26:33 -07:00
Robert Soriano
a926968f13 feat(deps): bump @nuxt/kit from 3.0.0-rc.5 to 3.0.0-rc.8 2022-08-14 12:25:48 -07:00
Robert Soriano
a8eba5ab8d feat(deps): bump @trpc/server from 9.26.0 to 9.27.0 2022-08-14 12:25:14 -07:00
Robert Soriano
5871528d67 feat(deps): bump @trpc/client from 9.26.0 to 9.27.0 2022-08-14 12:24:52 -07:00
Robert Soriano
def9d0b19d volta pin node@16 2022-08-14 12:24:20 -07:00
Robert Soriano
34f50fc4d6 chore(deps): bump pnpm to 7.5.0 2022-07-14 19:04:19 -07:00
Robert Soriano
9fe1469eba release v0.2.4 2022-07-14 19:03:09 -07:00
Robert Soriano
e48d7cd42c feat(deps): bump @nuxt/kit from 3.0.0-rc.4 to 3.0.0-rc.5 2022-07-14 19:02:54 -07:00
Robert Soriano
2b517ad77b release v0.2.3 2022-07-14 11:19:28 -07:00
Robert Soriano
77ee994e13 update deps 2022-07-14 11:19:19 -07:00
Robert Soriano
67a8a2a3ed feat(deps): bump ufo from 0.8.4 to 0.8.5 2022-07-14 11:19:01 -07:00
Robert Soriano
556b5c8e08 feat(deps): bump @vueuse/nuxt from 8.7.4 to 8.9.3 2022-07-14 11:18:37 -07:00
Robert Soriano
85154f12d7 feat(deps): bump @vueuse/core from 8.7.4 to 8.9.3 2022-07-14 11:18:21 -07:00
Robert Soriano
26ce46e12b feat(deps): bump @trpc/server from 9.25.3 to 9.26.0 2022-07-14 11:17:51 -07:00
Robert Soriano
c2cfd9a214 feat(deps): bump @trpc/client from 9.25.3 to 9.26.0 2022-07-14 11:17:34 -07:00
Robert Soriano
f736545128 ci: add release workflow 2022-07-14 11:16:43 -07:00
Robert Soriano
59a9eb5f95 fix playground workspace 2022-07-05 20:24:40 -07:00
Robert Soriano
48fc19076f Update README.md 2022-06-23 10:03:48 -07:00
Robert Soriano
6b83e5d4ea chore(deps): bump @trpc/server to 9.25.3 2022-06-18 21:06:25 -07:00
Robert Soriano
0358a258d7 chore(deps): bump @trpc/client to 9.25.3 2022-06-18 21:06:11 -07:00
Robert Soriano
8e7f33c9d2 chore(deps): bump h3 to 0.7.10 2022-06-18 21:05:48 -07:00
Robert Soriano
b8f4af2df3 chore(deps): bump @vueuse/nuxt to 8.7.4 2022-06-18 21:05:15 -07:00
Robert Soriano
666751678c chore(deps): bump @vueuse/core to 8.7.4 2022-06-18 21:04:56 -07:00
Robert Soriano
cab89aaa66 chore(deps): bump bumpp to 7.2.0 2022-06-18 21:04:17 -07:00
Robert Soriano
4c9850fafc chore(deps): add @types/dedent 2022-06-18 21:02:44 -07:00
Robert Soriano
07693009f8 release v0.2.2 2022-06-16 06:49:32 -07:00
Robert Soriano
dceade155c chore(deps): bump nuxt to 3.0.0-rc.4 2022-06-16 06:48:56 -07:00
Robert Soriano
4012249f09 chore(deps): bump h3 to 0.7.9 2022-06-16 06:48:20 -07:00
Robert Soriano
38eda2c4d6 chore(deps): bump @trpc/client to 9.25.2 2022-06-16 06:47:39 -07:00
Robert Soriano
9468af75f4 chore(deps): bump @trpc/server to 9.25.2 2022-06-16 06:47:18 -07:00
Robert Soriano
ea24a67a6d chore(deps): bump @nuxt/kit to 3.0.0-rc.4 2022-06-16 06:46:35 -07:00
Robert Soriano
5e8d04741c release v0.2.1 2022-06-13 18:51:41 -07:00
Robert Soriano
1032e65a0d refactor: remove unused import in playground 2022-06-13 18:51:35 -07:00
Robert Soriano
f261d3feeb Merge pull request #12 from cawa-93/patch-1
fix: auto-import `getQueryKey`
2022-06-13 18:49:07 -07:00
Alex Kozack
b1ca09e986 fix: auto-import getQueryKey 2022-06-13 12:06:03 +03:00
Robert Soriano
b804429fc0 update readme 2022-06-12 23:32:55 -07:00
Robert Soriano
7df64296ff update readme 2022-06-12 23:32:41 -07:00
Robert Soriano
a53d823f5e release v0.2.0 2022-06-12 23:30:07 -07:00
Robert Soriano
feef3dde6b update readme 2022-06-12 23:30:03 -07:00
Robert Soriano
82ee2ce672 refactor!: replace trpcURL option with endpoint 2022-06-12 23:29:28 -07:00
Robert Soriano
f62a13766a update readme 2022-06-12 23:19:55 -07:00
Robert Soriano
c7888e81ed refactor: use addAutoImport 2022-06-12 23:18:38 -07:00
Robert Soriano
c77eb68f5d refactor: rename endpoint option 2022-06-12 23:17:13 -07:00
Robert Soriano
333539569c refactor: lint 2022-06-12 23:16:01 -07:00
Robert Soriano
e5c40f183b Merge pull request #7 from cawa-93/feat/extract-get-query-key
feat: extract `getQueryKey` helper
2022-06-12 23:08:21 -07:00
Robert Soriano
977a9e1465 release v0.1.24 2022-06-05 17:05:34 -07:00
Robert Soriano
2620379e02 feat: allow changing of src dir 2022-06-05 17:05:13 -07:00
Robert Soriano
e4f42d5322 Merge pull request #10 from nicolesmileyface/feat/support-src-dir
use nuxt srcDir instead of rootDir
2022-06-05 17:02:41 -07:00
Cole Hollant
ad28a9124e use nuxt srcDir instead of rootDir 2022-06-05 11:55:25 -04:00
cawa-93
273bda980b feat: extract getQueryKey helper
This method will be useful to get a key that can be used to reset the internal nuxt cache
2022-05-31 15:20:21 +03:00
Robert Soriano
9c8509f79c release v0.1.23 2022-05-27 08:53:18 -07:00
Robert Soriano
2ce29137ce fix: unimported composable 2022-05-27 08:41:14 -07:00
Robert Soriano
e9c5307e23 release v0.1.22 2022-05-27 01:10:10 -07:00
Robert Soriano
bbdabf544c fix: type inference bug with vueuse 2022-05-27 01:10:06 -07:00
Robert Soriano
aed10ac5b8 release v0.1.21 2022-05-27 00:56:58 -07:00
Robert Soriano
4b2c714658 fix: remove module export 2022-05-27 00:56:53 -07:00
Robert Soriano
43e9fefdbd release v0.1.20 2022-05-26 13:48:57 -07:00
Robert Soriano
dabda23976 export module id 2022-05-26 13:48:53 -07:00
Robert Soriano
af89f32275 release v0.1.19 2022-05-26 07:27:20 -07:00
Robert Soriano
1f27b871fb fix: unref import from vue 2022-05-26 07:21:42 -07:00
Robert Soriano
281e4c05a0 release v0.1.18 2022-05-24 10:01:23 -07:00
Robert Soriano
38ac520b97 fix: server storage and incorrect imports 2022-05-24 10:01:19 -07:00
Robert Soriano
96ebff619e release v0.1.17 2022-05-24 09:55:59 -07:00
Robert Soriano
f668e4a9b4 docs: update readme 2022-05-24 09:55:35 -07:00
Robert Soriano
58b0165557 update readme 2022-05-24 09:55:05 -07:00
Robert Soriano
3b5e35ef68 feat: add composable for custom client headers 2022-05-24 09:46:15 -07:00
Robert Soriano
48152ead8d refactor: remove some comments 2022-05-24 06:43:42 -07:00
Robert Soriano
7f44e049c0 release v0.1.16 2022-05-23 12:41:55 -07:00
Robert Soriano
5a71bbf1fe refactor: use trpc url from config 2022-05-23 12:41:49 -07:00
Robert Soriano
285487e9bf release v0.1.15 2022-05-23 11:24:10 -07:00
Robert Soriano
2cfa64fcc6 fix: unresolvable runtime config 2022-05-23 11:24:07 -07:00
Robert Soriano
eea5733dcd release v0.1.14 2022-05-23 11:09:16 -07:00
Robert Soriano
71bbbf2b86 fix: trpc client composable type 2022-05-23 11:08:52 -07:00
Robert Soriano
2b57ab8791 release v0.1.13 2022-05-23 11:06:58 -07:00
Robert Soriano
f8edd769f0 fix: trpc client composable type 2022-05-23 11:06:51 -07:00
Robert Soriano
419ef34de6 release v0.1.12 2022-05-23 11:03:53 -07:00
Robert Soriano
30c76b5859 fix: client type declaration 2022-05-23 11:03:50 -07:00
Robert Soriano
2575beae5d release v0.1.11 2022-05-23 10:42:19 -07:00
Robert Soriano
b09d1af30d fix: client type declaration 2022-05-23 10:42:16 -07:00
Robert Soriano
610e441db7 release v0.1.10 2022-05-23 10:31:53 -07:00
Robert Soriano
959b370729 fix: unable to resolve nuxt instance 2022-05-23 10:31:48 -07:00
Robert Soriano
c1c4e67694 release v0.1.9 2022-05-23 10:26:02 -07:00
Robert Soriano
779221d9e6 refactor: plugin arrangement 2022-05-23 10:25:34 -07:00
Robert Soriano
986b661e99 release v0.1.8 2022-05-23 10:04:13 -07:00
Robert Soriano
77325a6699 fix: headers missing 2022-05-23 10:04:06 -07:00
Robert Soriano
6dcb4ce8a6 refactor: remove useless transpiled build 2022-05-20 14:26:45 -07:00
Robert Soriano
c95d46f43a release v0.1.7 2022-05-20 11:44:02 -07:00
Robert Soriano
2844cc0bbd refactor: remove helper types 2022-05-20 11:43:58 -07:00
Robert Soriano
aeb2e1b8e3 docs: add inference helpers recipe 2022-05-20 11:42:40 -07:00
Robert Soriano
68d9eb2461 release v0.1.6 2022-05-20 11:30:09 -07:00
Robert Soriano
109a07a42d feat: add helper types 2022-05-20 11:30:04 -07:00
Robert Soriano
7775e59b0c release v0.1.5 2022-05-20 11:26:48 -07:00
Robert Soriano
c23af214a3 remove helpers 2022-05-20 11:26:43 -07:00
Robert Soriano
7851846ad5 release v0.1.4 2022-05-20 11:22:30 -07:00
Robert Soriano
f9b0aa002e refactor: type helpers 2022-05-20 11:22:19 -07:00
Robert Soriano
eb1bd0c700 feat: add trpc type helpers 2022-05-20 11:07:12 -07:00
Robert Soriano
47ba58c0b7 release v0.1.3 2022-05-20 10:19:55 -07:00
Robert Soriano
a458ed9b3f refactor: use full path for router type 2022-05-20 10:08:53 -07:00
Robert Soriano
09d043d49b docs: add recommended ide setup 2022-05-20 10:04:48 -07:00
Robert Soriano
3e47e2a389 Update README.md 2022-05-20 09:14:40 -07:00
Robert Soriano
f8d82c4af1 remove unused module 2022-05-20 08:18:29 -07:00
Robert Soriano
e31578bf97 refactor: remove unused transpile 2022-05-20 00:58:27 -07:00
44 changed files with 6338 additions and 2921 deletions

22
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

142
README.md
View File

@@ -4,147 +4,11 @@
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" />
<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>
Learn more about tRPC.io [here](https://trpc.io/docs/v10).
## Install
## Recommended IDE Setup
```bash
npm i trpc-nuxt
```
```ts
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
modules: ['trpc-nuxt'],
trpc: {
baseURL: 'http://localhost:3000', // defaults to http://localhost:3000
trpcURL: '/api/trpc', // defaults to /api/trpc
},
typescript: {
strict: true // set this to true 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
})
```
## 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 { CompatibilityEvent } 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: CompatibilityEvent) => {
// ...
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)
Learn more about tRPC.io [here](https://trpc.io/docs).
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## License

1
api.d.ts vendored
View File

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

1
client.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './dist/client/index'

1
docs/README.md Normal file
View File

@@ -0,0 +1 @@
docs

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

@@ -0,0 +1,5 @@
export default defineAppConfig({
docus: {
title: 'tRPC Nuxt',
},
})

View File

@@ -0,0 +1,90 @@
---
title: Installation
description: tRPC-Nuxt provides first class integration with tRPC.
---
# Installation
## 1. Add to existing Nuxt project
::code-group
```bash [pnpm]
pnpm add @trpc/server@next @trpc/client@next trpc-nuxt@next zod
```
```bash [npm]
npm install @trpc/server@next @trpc/client@next trpc-nuxt@next zod
```
```bash [yarn]
yarn add @trpc/server@next @trpc/client@next trpc-nuxt@next 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.
## 2. Enable strict mode
If you want to use Zod for input validation, make sure you have enabled strict mode:
::code-group
```json [tsconfig.json]
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true
}
}
```
```ts [nuxt.config.ts]
export default defineNuxtConfig({
typescript: {
strict: true
}
})
```
::
If strict mode is too much, at least enable `strictNullChecks`:
::code-group
```json [tsconfig.json]
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strictNullChecks": true
}
}
```
```ts [nuxt.config.ts]
export default defineNuxtConfig({
typescript: {
tsConfig: {
strictNullChecks: true
}
}
})
```
::
## Next Steps
Now that you've installed the required dependencies, you are ready to start building your application.

View File

@@ -0,0 +1,140 @@
---
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 { TRPCError, 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 '..'
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 '../../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 set of strongly-typed composables using your API's type signature.
```ts [plugins/client.ts]
import { httpBatchLink } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin(() => {
const client = createTRPCNuxtProxyClient<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()
// query and mutate uses useAsyncData under the hood
const { data, pending, error } = await $client.hello.query({ text: 'client' })
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else>
<p>{{ hello.data?.greeting }}</p>
</div>
</template>
```

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: "A supa simple wrapper arousnd supabase-js to enable usage and integration within Nuxt."
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@next
---
#title
tRPC [Nuxt]{.text-primary-500}
#description
End-to-end typesafe APIs in Nuxt applications.
#extra
::list
- Automatic typesafety
- Snappy DX
- Autocompletion
::
::

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

@@ -0,0 +1,16 @@
export default defineNuxtConfig({
app: {
pageTransition: false,
layoutTransition: false,
},
extends: '@nuxt-themes/docus',
build: {
transpile: [/content-edge/],
},
nitro: {
prerender: {
crawlLinks: true,
routes: ['/'],
},
},
})

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"
},
"devDependencies": {
"@docus/github": "npm:@docus/github-edge@latest",
"@nuxt-themes/docus": "npm:@nuxt-themes/docus-edge@0.1.0-2a7c428",
"@nuxt-themes/website": "0.1.7",
"nuxt": "^3.0.0-rc.12",
"vue-plausible": "^1.3.2"
}
}

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

@@ -0,0 +1,25 @@
import { defineTheme } from 'pinceau'
export default defineTheme({
title: 'asddsasda 3s',
cover: {
src: 'https://res.cloudinary.com/nuxt/image/upload/v1650870623/nuxt3-rc-social_z6qh3m.png',
alt: 'Nuxt 3 cover image',
},
aside: {
level: 1,
},
// colors: {
// primary: {
// 100: '#77b0db',
// 200: '#589ed3',
// 300: '#4e98d0',
// 400: '#398ccb',
// 500: '#398ccb',
// 600: '#2BAB71',
// 700: '#317eb9',
// 800: '#2e77af',
// 900: '#266290',
// },
// },
})

3
docs/tsconfig.json Normal file
View File

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

View File

@@ -1,61 +1,60 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.1.2",
"packageManager": "pnpm@7.1.1",
"version": "0.4.0-beta.3",
"packageManager": "pnpm@7.5.0",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"sideEffects": false,
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./api": {
"import": "./dist/runtime/api.mjs",
"types": "./dist/runtime/api.d.ts"
"./client": {
"require": "./dist/client/index.js",
"import": "./dist/client/index.mjs"
}
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist",
"*.d.ts"
"client.d.ts"
],
"scripts": {
"prepublishOnly": "nr build",
"build": "nuxt-module-build",
"play": "nr build && nuxi dev playground",
"build:playground": "nuxi build playground",
"build": "tsup",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag && pnpm publish",
"prepare": "nuxi prepare playground"
"release": "bumpp --commit --push --tag && npm publish"
},
"peerDependencies": {
"@trpc/client": "^10.0.0-proxy-beta.21",
"@trpc/server": "^10.0.0-proxy-beta.21",
"nuxt": "^3.0.0-rc.12"
},
"dependencies": {
"@nuxt/kit": "^3.0.0-rc.3",
"@trpc/client": "^9.23.3",
"@trpc/server": "^9.23.2",
"defu": "^6.0.0",
"fs-extra": "^10.1.0",
"h3": "^0.7.8",
"pathe": "^0.3.0",
"ufo": "^0.8.4"
"h3": "^0.8.5",
"ohash": "^0.1.5",
"ufo": "^0.8.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.23.1",
"@antfu/ni": "^0.16.2",
"@nuxt/module-builder": "latest",
"@types/fs-extra": "^9.0.13",
"bumpp": "^7.1.1",
"eslint": "^8.14.0",
"nuxt": "^3.0.0-rc.3",
"ohash": "^0.1.0",
"pnpm": "^7.1.1",
"superjson": "^1.9.1",
"trpc-nuxt": "workspace:*",
"zod": "^3.16.0"
"@antfu/eslint-config": "^0.27.0",
"@antfu/ni": "^0.18.3",
"@trpc/client": "10.0.0-rc.2",
"@trpc/server": "10.0.0-rc.2",
"bumpp": "^8.2.1",
"eslint": "^8.25.0",
"nuxt": "3.0.0-rc.12",
"pnpm": "^7.5.0",
"tsup": "6.0.1",
"typescript": "^4.7.4"
},
"eslintConfig": {
"extends": "@antfu"
"extends": "@antfu",
"rules": {
"no-console": "warn"
}
}
}

View File

@@ -1,11 +1,7 @@
import { defineNuxtConfig } from 'nuxt'
import Module from '../src/module'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [Module],
runtimeConfig: {
baseURL: 'http://localhost:3000',
baseURL: '',
},
typescript: {
strict: true,

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.0.0-rc.2",
"@trpc/server": "10.0.0-rc.2",
"superjson": "^1.11.0",
"trpc-nuxt": "workspace:*",
"zod": "^3.19.1"
},
"devDependencies": {
"nuxt": "3.0.0-rc.12"
}
}

View File

@@ -1,46 +1,55 @@
<script setup lang="ts">
const client = useClient()
const { data: todos, pending, error, refresh } = await useAsyncQuery(['getTodos'])
const { $client } = useNuxtApp()
// const headers = useClientHeaders()
// const addHeader = () => {
// headers.value.authorization = 'Bearer abcdefghijklmnop'
// console.log(headers.value)
// }
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,
})
console.log('Todo: ', result)
console.log(x.data.value)
}
catch (e) {
console.log(e)
}
}
const { data: todos, pending, error, refresh } = await $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>
<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 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 $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,50 @@
import { httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import superjson from 'superjson'
import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin((nuxtApp) => {
const client = createTRPCNuxtProxyClient<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',
/**
* Set custom request headers on every request from tRPC
* @link https://trpc.io/docs/ssr
*/
headers() {
if (nuxtApp.ssrContext?.event?.req) {
// To use SSR properly, you need to forward the client's headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
// If you're using Node 18, omit the "connection" header
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
connection: _connection,
...headers
} = nuxtApp.ssrContext.event.req.headers
return {
...headers,
// Optional: inform server that it's an SSR request
'x-ssr': '1',
}
}
return {}
},
}),
],
})
return {
provide: {
client,
},
}
})

View File

@@ -0,0 +1,23 @@
import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '../../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,17 @@
/* 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 async function createContext(
opts: H3Event,
) {
// for API-response caching see https://trpc.io/docs/caching
return {}
}

View File

@@ -1,35 +0,0 @@
import * as trpc from '@trpc/server'
import { z } from 'zod'
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()
.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) {
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})

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,29 @@
import { initTRPC } from '@trpc/server'
import superjson from 'superjson'
import type { Context } from './context'
const t = initTRPC.context<Context>().create({
transformer: superjson,
})
/**
* 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"
}

7753
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,3 @@
packages:
- playground/*
- 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 { CompatibilityEvent } 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 }: CompatibilityEvent) {
// 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,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).

73
src/client/index.ts Normal file
View File

@@ -0,0 +1,73 @@
import type { CreateTRPCClientOptions, inferRouterProxyClient } from '@trpc/client'
import { createTRPCProxyClient } from '@trpc/client'
import type {
AnyRouter,
} from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash'
import type { DecoratedProcedureRecord } from './types'
/**
* Calculates the key used for `useAsyncData` call
*/
export function getQueryKey(
path: string,
input: unknown,
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}
/**
* @internal
*/
export function createNuxtProxyDecoration<TRouter extends AnyRouter>(name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => {
const args = opts.args
const pathCopy = [name, ...opts.path]
// The last arg is for instance `.mutate` or `.query()`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const lastArg = pathCopy.pop()!
const path = pathCopy.join('.')
const [input, asyncDataOptions] = args
const queryKey = getQueryKey(path, input)
return useAsyncDataWithError(queryKey, () => (client as any)[path][lastArg](input), asyncDataOptions)
})
}
/**
* Custom useAsyncData to add server error to client
*/
async function useAsyncDataWithError(queryKey: string, cb: any, asyncDataOptions: any) {
// @ts-ignore: nuxt internal
const serverError = useState(`error-${queryKey}`, () => null)
// @ts-ignore: nuxt internal
const { error, data, ...rest } = await useAsyncData(queryKey, cb, asyncDataOptions)
if (error.value && !serverError.value)
serverError.value = error.value as any
if (data.value)
serverError.value = null
return {
...rest,
data,
error: serverError,
}
}
export function createTRPCNuxtProxyClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient(opts)
const decoratedClient = createFlatProxy((key) => {
return createNuxtProxyDecoration(key, client)
}) as DecoratedProcedureRecord<TRouter['_def']['record']>
return decoratedClient
}

64
src/client/types.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { TRPCClientErrorLike } from '@trpc/client'
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
AnyRouter,
ProcedureRouterRecord,
inferProcedureInput,
inferProcedureOutput,
} from '@trpc/server'
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
// Inspired by trpc/react-query client types
// https://github.com/trpc/trpc/blob/next/packages/react-query/src/createTRPCReact.tsx
/**
* @internal
*/
export type DecorateProcedure<
TProcedure extends AnyProcedure,
TPath extends string,
> = TProcedure extends AnyQueryProcedure
? {
query: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys>,
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : TProcedure extends AnyMutationProcedure ? {
mutate: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys>,
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : never
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TPath extends string = '',
> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<
TProcedures[TKey]['_def']['record'],
`${TPath}${TKey & string}.`
>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey], `${TPath}${TKey & string}`>
: never;
}

1
src/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './server'

View File

@@ -1,83 +0,0 @@
import { fileURLToPath } from 'url'
import { join } from 'pathe'
import { defu } from 'defu'
import { addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
trpcURL: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'trpc-nuxt',
configKey: 'trpc',
},
defaults: {
baseURL: 'http://localhost:3000',
trpcURL: '/api/trpc',
},
async setup(options, nuxt) {
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-client', '#build/trpc-handler')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
nuxt.options.build.transpile.push(handlerPath)
// Final resolved configuration
const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, {
baseURL: options.baseURL,
trpcURL: options.trpcURL,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: '#build/trpc-client' },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
)
})
addServerHandler({
route: `${finalConfig.trpcURL}/*`,
handler: handlerPath,
})
addTemplate({
filename: 'trpc-client.ts',
write: true,
getContents() {
return `
import * as trpc from '@trpc/client'
import type { router } from '~/server/trpc'
const client = trpc.createTRPCClient<typeof router>({
url: '${finalConfig.baseURL}${finalConfig.trpcURL}',
})
export const useClient = () => client
`
},
})
addTemplate({
filename: 'trpc-handler.ts',
write: true,
getContents() {
return `
import { createTRPCHandler } from 'trpc-nuxt/api'
import { useRuntimeConfig } from '#imports'
import * as functions from '~/server/trpc'
const { trpc: { trpcURL } } = useRuntimeConfig().public
export default createTRPCHandler({
...functions,
trpcURL
})
`
},
})
},
})

View File

@@ -1,60 +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 { TRPCClientErrorLike } from '@trpc/client'
import { objectHash } from 'ohash'
import { useAsyncData, useState } from '#app'
import { useClient } from '#build/trpc-client'
import type { router } from '~/server/trpc'
type AppRouter = typeof router
type inferProcedures<
TObj extends ProcedureRecord<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>
output: inferProcedureOutput<TObj[TPath]>
};
}
type TQueries = AppRouter['_def']['queries']
type TError = TRPCClientErrorLike<AppRouter>
type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
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 = useClient()
const key = `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
const serverError = useState<TError | null>(`error-${key}`, () => null)
const { error, data, ...rest } = await useAsyncData(
key,
() => client.query(...pathAndInput),
options,
)
if (process.server && error.value && !serverError.value)
serverError.value = error.value as any
if (data.value)
serverError.value = null
return {
...rest,
data,
error: serverError,
} as any
}

View File

@@ -1,20 +1,20 @@
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,
} from '@trpc/server'
import { createURL } from 'ufo'
import type { CompatibilityEvent } from 'h3'
import { defineEventHandler, isMethod, useBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/dist/declarations/src/rpc'
import type { H3Event } from 'h3'
import { 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: CompatibilityEvent) => MaybePromise<inferRouterContext<TRouter>>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
@@ -30,25 +30,25 @@ export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: CompatibilityEvent['req']
req: H3Event['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export function createTRPCHandler<Router extends AnyRouter>({
export function createNuxtApiHandler<TRouter extends AnyRouter>({
router,
createContext,
responseMeta,
onError,
trpcURL,
url = '/api/trpc',
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
trpcURL: string
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
url?: string
}) {
return defineEventHandler(async (event) => {
const {
@@ -56,17 +56,17 @@ export function createTRPCHandler<Router extends AnyRouter>({
res,
} = event
const $url = createURL(req.url)
const $url = createURL(req.url!)
const httpResponse = await resolveHTTPResponse({
router,
req: {
method: req.method,
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await useBody(event),
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams,
},
path: $url.pathname.substring(trpcURL.length + 1),
path: $url.pathname.substring(url.length + 1),
createContext: async () => createContext?.(event),
responseMeta,
onError: (o) => {
@@ -81,8 +81,8 @@ export function createTRPCHandler<Router extends AnyRouter>({
res.statusCode = status
Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key])
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body

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

10
tsup.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts', 'src/client/index.ts'],
format: ['cjs', 'esm'],
splitting: false,
clean: true,
external: ['#app'],
dts: true,
})