mirror of
https://github.com/ArthurDanjou/trpc-nuxt.git
synced 2026-01-23 08:20:37 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f061b0531f | ||
|
|
f08b724df7 | ||
|
|
4085d5fe26 | ||
|
|
13713343ce | ||
|
|
d6f11bb301 | ||
|
|
c5c1f1cdba | ||
|
|
0681dd554d | ||
|
|
e7a76694af | ||
|
|
da75c18ebe | ||
|
|
6289575900 | ||
|
|
ac212672e4 | ||
|
|
b8e2a83e26 | ||
|
|
fd98b0d414 | ||
|
|
20ab235856 | ||
|
|
7205c356f8 | ||
|
|
e14827e0c7 | ||
|
|
eea6406d5a | ||
|
|
2d3409a25b | ||
|
|
7a71a6e128 | ||
|
|
d8e89297cd | ||
|
|
6768205318 | ||
|
|
c20a092a31 | ||
|
|
64df8fb7ad | ||
|
|
d5567e5826 | ||
|
|
6b898e836c |
11
README.md
11
README.md
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
|
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<figure>
|
||||||
|
<img src="https://i.imgur.com/3AZlBZH.gif" alt="Demo" />
|
||||||
|
<figcaption>
|
||||||
|
<p align="center">
|
||||||
|
The client above is <strong>not</strong> importing any code from the server, only its type declarations.
|
||||||
|
</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</p>
|
||||||
|
|
||||||
Docs: https://trpc-nuxt.vercel.app
|
Docs: https://trpc-nuxt.vercel.app
|
||||||
|
|
||||||
For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3).
|
For version 3 of this module (tRPC v9, auto-imports, auto handlers), [go here](https://github.com/wobsoriano/trpc-nuxt/tree/v3).
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ export default defineAppConfig({
|
|||||||
alt: 'tRPC-Nuxt cover',
|
alt: 'tRPC-Nuxt cover',
|
||||||
url: 'https://trpc-nuxt.vercel.app',
|
url: 'https://trpc-nuxt.vercel.app',
|
||||||
debug: false,
|
debug: false,
|
||||||
|
socials: {
|
||||||
|
github: 'wobsoriano/trpc-nuxt'
|
||||||
|
},
|
||||||
aside: {
|
aside: {
|
||||||
level: 1
|
level: 1
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
credits: {
|
credits: true,
|
||||||
icon: 'IconDocus',
|
|
||||||
text: 'Powered by Docus',
|
|
||||||
href: 'https://docus.com'
|
|
||||||
},
|
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
label: 'NuxtJS',
|
label: 'NuxtJS',
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ For making typesafe API calls from your client.
|
|||||||
|
|
||||||
Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required.
|
Most examples use [Zod](https://github.com/colinhacks/zod) for input validation and tRPC.io highly recommends it, though it isn't required.
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
Now that you've installed the required dependencies, you are ready to start building your application.
|
Now that you've installed the required dependencies, you are ready to start building your application.
|
||||||
|
|||||||
40
docs/content/1.get-started/3.tips/1.composables.md
Normal file
40
docs/content/1.get-started/3.tips/1.composables.md
Normal 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.
|
||||||
84
docs/content/1.get-started/3.tips/2.server-side-calls.md
Normal file
84
docs/content/1.get-started/3.tips/2.server-side-calls.md
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::
|
||||||
24
docs/content/1.get-started/3.tips/3.aborting-procedures.md
Normal file
24
docs/content/1.get-started/3.tips/3.aborting-procedures.md
Normal 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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
104
docs/content/1.get-started/3.tips/4.authorization.md
Normal file
104
docs/content/1.get-started/3.tips/4.authorization.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Create context from request headers
|
||||||
|
|
||||||
|
```ts [server/trpc/context.ts]
|
||||||
|
import type { H3Event } from 'h3'
|
||||||
|
import { inferAsyncReturnType } from '@trpc/server'
|
||||||
|
import { decodeAndVerifyJwtToken } from './somewhere/in/your/app/utils'
|
||||||
|
|
||||||
|
export async function createContext({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
}: 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
|
||||||
|
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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
4
docs/content/1.get-started/index.md
Normal file
4
docs/content/1.get-started/index.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
navigation: false
|
||||||
|
redirect: /get-started/installation
|
||||||
|
---
|
||||||
@@ -24,8 +24,8 @@ End-to-end typesafe APIs in Nuxt applications.
|
|||||||
|
|
||||||
#extra
|
#extra
|
||||||
::list
|
::list
|
||||||
- Automatic typesafety
|
- Automatic typesafety
|
||||||
- Snappy DX
|
- Snappy DX
|
||||||
- Autocompletion
|
- Autocompletion on the client, for inputs, outputs and errors
|
||||||
::
|
::
|
||||||
::
|
::
|
||||||
|
|||||||
@@ -8,12 +8,10 @@
|
|||||||
"preview": "nuxi preview"
|
"preview": "nuxi preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/head": "^1.0.3",
|
"nuxt": "^3.0.0"
|
||||||
"nuxt": "^3.0.0-rc.13",
|
|
||||||
"@nuxtjs/tailwindcss": "^6.1.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt-themes/docus": "npm:@nuxt-themes/docus-edge@latest",
|
"@nuxt-themes/docus": "^0.3.1",
|
||||||
"@nuxtlabs/github-module": "npm:@nuxtlabs/github-module-edge@latest"
|
"@nuxtlabs/github-module": "^1.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "trpc-nuxt",
|
"name": "trpc-nuxt",
|
||||||
"description": "End-to-end typesafe APIs in Nuxt applications.",
|
"description": "End-to-end typesafe APIs in Nuxt applications.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -31,17 +31,15 @@
|
|||||||
"@trpc/server": "^10.0.0-proxy-beta.21"
|
"@trpc/server": "^10.0.0-proxy-beta.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"h3": "^0.8.6",
|
"h3": "^1.0.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"nuxt": "^3.0.0-rc.13",
|
"ohash": "^1.0.0",
|
||||||
"ohash": "^0.1.5",
|
"ufo": "^1.0.0"
|
||||||
"ufo": "^0.8.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/kit": "3.0.0-rc.13",
|
|
||||||
"@nuxtjs/eslint-config-typescript": "^11.0.0",
|
"@nuxtjs/eslint-config-typescript": "^11.0.0",
|
||||||
"@trpc/client": "10.0.0-rc.7",
|
"@trpc/client": "10.0.0-rc.8",
|
||||||
"@trpc/server": "10.0.0-rc.7",
|
"@trpc/server": "10.0.0-rc.8",
|
||||||
"bumpp": "^8.2.1",
|
"bumpp": "^8.2.1",
|
||||||
"concurrently": "^7.5.0",
|
"concurrently": "^7.5.0",
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^8.25.0",
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trpc/client": "10.0.0-rc.7",
|
"@trpc/client": "10.0.0-rc.8",
|
||||||
"@trpc/server": "10.0.0-rc.7",
|
"@trpc/server": "10.0.0-rc.8",
|
||||||
"superjson": "^1.11.0",
|
"superjson": "^1.11.0",
|
||||||
"trpc-nuxt": "workspace:*",
|
"trpc-nuxt": "workspace:*",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nuxt": "^3.0.0-rc.13"
|
"nuxt": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { TRPCClientError } from '@trpc/client';
|
||||||
|
import type { inferRouterOutputs } from '@trpc/server';
|
||||||
|
import type { AppRouter } from '~~/server/trpc/routers';
|
||||||
|
|
||||||
const { $client } = useNuxtApp()
|
const { $client } = useNuxtApp()
|
||||||
// const headers = useClientHeaders()
|
// const headers = useClientHeaders()
|
||||||
|
|
||||||
@@ -23,7 +27,10 @@ const addTodo = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: todos, pending, error, refresh } = await useAsyncData(() => $client.todo.getTodos.query())
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
2086
pnpm-lock.yaml
generated
2086
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ export interface OnErrorPayload<TRouter extends AnyRouter> {
|
|||||||
error: TRPCError
|
error: TRPCError
|
||||||
type: ProcedureType | 'unknown'
|
type: ProcedureType | 'unknown'
|
||||||
path: string | undefined
|
path: string | undefined
|
||||||
req: H3Event['req']
|
req: H3Event['node']['req']
|
||||||
input: unknown
|
input: unknown
|
||||||
ctx: undefined | inferRouterContext<TRouter>
|
ctx: undefined | inferRouterContext<TRouter>
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ export function createNuxtApiHandler<TRouter extends AnyRouter> ({
|
|||||||
const {
|
const {
|
||||||
req,
|
req,
|
||||||
res
|
res
|
||||||
} = event
|
} = event.node
|
||||||
|
|
||||||
const $url = createURL(req.url!)
|
const $url = createURL(req.url!)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user