Nuxt + Nitro Type Inference: tRPC-like Safety Without the Setup

Nuxt auto-imports server route types into the client. You get end-to-end type safety between your API and your frontend with no extra library, no code generation, and no schema to maintain.

·4 min read

Write an API route on the server. Call it with useFetch or $fetch on the client. TypeScript knows the exact shape of the response-no tRPC, no code generation, no extra configuration.

If you have used tRPC with React, Nuxt gives you the same thing with less setup. The difference is it just works without a separate router, adapter, and provider to wire up.

How it works

Nuxt uses Nitro as its server engine. Nitro can infer the return type of each API route and expose that information to the Nuxt client layer.

A server route in server/api/posts.get.ts:

export default defineEventHandler(async () => {
  const posts = await db.select().from(postsTable)
  return posts
})

On the client, useFetch automatically knows what this returns:

// data is typed as Post[] - no type annotation needed
const { data } = await useFetch('/api/posts')

TypeScript infers the return type directly from the handler. If you change the shape of what the route returns, TypeScript will flag every call site that now mismatches.

The mechanism

Nuxt generates TypeScript declarations for all your server routes into .nuxt/types/nitro.d.ts. These declarations map route paths to their inferred response types. useFetch is typed to consume these declarations, so the return type of a useFetch call flows directly from the server handler’s return type.

No schema. No codegen step you have to remember to run. No separate client package to install. The types update when you save the file.

Typed route params and query

The inference goes further. Handler parameters are typed too.

// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
  const { id } = getRouterParams(event)  // typed as string
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, Number(id)),
  })
  if (!post) throw createError({ statusCode: 404 })
  return post
})
// client - data is Post | null
const { data } = await useFetch(`/api/posts/${postId}`)

Query params, body parsing with readBody<T>, and headers all have typed helpers too.

Compared to tRPC

tRPC solves the same problem for React/Next.js apps. You define procedures on the server, and the client calls them with full type safety. It is an excellent solution, especially for complex applications with many procedures.

The Nuxt approach is more lightweight:

AspecttRPCNuxt + Nitro
SetupRouter, adapter, providerNothing extra
Client callstrpc.posts.query()useFetch('/api/posts')
Type sourceExported router typeAuto-generated declarations
REST compatibilityNo (RPC only)Yes (standard HTTP)
External API callsRequires wrapperPlain fetch works
ValidationZod/Valibot requiredOptional

For a Nuxt project, the built-in inference is the right default. You get type safety across the stack and your API stays a standard REST API that any client can call, not a tRPC-specific endpoint.

Adding validation

The inference is inferred, not enforced. If you want runtime validation (which you should for external input), add it with a schema library.

import { z } from 'zod'

const CreatePostSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(10),
})

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, CreatePostSchema.parse)
  // body is typed as { title: string; content: string }
  const post = await db.insert(posts).values(body).returning()
  return post[0]
})

readValidatedBody is a Nitro utility that validates and returns a typed body. The validated type flows through to the client automatically.

What this means in practice

You write server code, call it from the client, TypeScript keeps them in sync. No runtime schema management, no extra tooling to maintain.

It is less explicit than tRPC but also invisible-the types just work, and you only notice them when something changes on the server and the client immediately flags it.