@nxgt/openapi-nuxt
| Install | |
|---|---|
npm install @nxgt/openapi-nuxt |
|
| Latest Version: | 0.3.3 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 22, 2026 |
| Links: | GitHub · npm |
@nxgt/openapi-nuxt
A Nuxt module for the operations
@nxgt/openapi-codegen
generates, with one spec for both ends of the app:
- The server. Your Hono app, typically built with
@nxgt/openapi-hono, is served by Nitro under a prefix,/apiby default. - The client.
useApi()is auto-imported. It is a client bound to the spec through@nxgt/openapi-httpyz, so its paths, inputs and replies are typed. - SSR in process. While a page renders on the server,
useApi()calls the Hono app without leaving the process, with the incoming request's cookies and headers. In the browser, it calls the same origin. - Presets.
useApiData()isuseAsyncDatawith the client, andcreateHonoApp()is a Hono app whosec.env.eventis the h3 event. - TanStack Query, with
query: true.useApiQuery()anduseApiMutation()areuseQueryanduseMutationof an operation. The cache SSR filled goes to the browser.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nxgt/openapi-nuxt'],
openapi: {
server: 'server/app.ts',
operations: 'server/generated/operations.ts',
},
});
<script setup lang="ts">
const { data: reply } = await useApiData((api) =>
api.get('/employees/{id}', { param: { id: 7 } }),
);
</script>
<template>
<p v-if="reply?.status === 200">{{ reply.data.name }}</p>
</template>
Install
bun add @nxgt/openapi-nuxt @nxgt/openapi-httpyz @nxgt/httpyz
bun add @nxgt/openapi-hono hono zod # for the server
bun add -d @nxgt/openapi-codegen
bun add @tanstack/vue-query @nxgt/httpyz-query # for query: true
nuxt 4, @nxgt/openapi-httpyz and @nxgt/httpyz are peers: the module
writes a client that imports them into your app. typescript 6 or 7
(^6.0.3 || ^7.0.0) is a required peer, as in every @nxgt package. hono is an optional peer,
for createHonoApp. @tanstack/vue-query and @nxgt/httpyz-query are
optional peers, for query. The module refuses to start with query set if
either is missing.
Setup
-
Generate the spec's code with
hono: true, somewhere the app can import it,server/generated/for instance:// openapi-codegen.config.ts import { defineConfig } from '@nxgt/openapi-codegen'; export default defineConfig({ input: 'openapi/openapi.yaml', output: 'server/generated', hono: true, }); -
Write the Hono app, with the spec's paths, without the prefix, and export it as the default:
// server/app.ts import { createRoutes } from './generated/hono'; const app = createHonoApp(); // auto-imported const routes = createRoutes(app); routes.get('/employees/{id}', async (c) => { const { id } = c.req.valid('param'); return c.json(await employees.find(id), 200); }); export default app; -
Add the module to
nuxt.config.ts, as above. Runnuxi prepare, or start the dev server, foruseApi()'s types to exist.
Usage
The server
A request to /api/employees/7 reaches the app as /employees/7: the module
takes the prefix off, then hands the request to app.fetch. The app answers
everything under the prefix, its own 404 included.
The app's env holds the h3 event, for what Nitro and its middleware keep on
it. createHonoApp() types it: it is
new Hono<{ Bindings: { event: H3Event } }>(), auto-imported in the
server's files, and importable from @nxgt/openapi-nuxt/hono elsewhere:
const app = createHonoApp();
app.get('/whoami', (c) => c.json({ user: c.env.event.context.user ?? null }));
// The spec's routes on it type it too.
createRoutes(app).get('/employees/{id}', async (c) => {
const user = c.env.event.context.user;
return c.json(await employees.find(c.req.valid('param').id, user), 200);
});
// With variables of your own, and Hono's options:
const typed = createHonoApp<{ Variables: { user: User } }>({ strict: false });
The client
useApi() is the client
createOpenApiClient
returns, bound to the generated table: api.get('/employees/{id}', …),
api.op('getEmployee', …), and the rest of its API.
- During SSR, the call goes through the request event's
fetch: Nitro runs the app in process, and the page's cookies and headers go with it, so a session reaches the API as it would from the browser. - In the browser, the call goes to the page's origin, under the prefix.
A reply is checked and decoded as createOpenApiClient does by default.
With dates: 'date', a Date survives the SSR payload: Nuxt serializes it.
Data for a page
useApiData() is useAsyncData whose handler receives the client:
const { data: reply, refresh } = await useApiData('employee', (api, { signal }) =>
api.get('/employees/{id}', { param: { id: id.value } }, { signal }),
);
// reply.value?.status === 200: reply.value.data is an Employee
// reply.value?.status === 404: reply.value.data is the declared 404's body
- The key is optional. Nuxt's compiler gives a call without one a key
of its own, as it does for
useAsyncData. - A reply comes back without its
Response, which the SSR payload cannot carry:{ status, type, data }, narrowed by its status. A handler that returns anything else,reply.datafor instance, gets it as it is. signalis aborted when Nuxt drops the call, on a newer refresh for instance. Pass it to the client's call.- The options are
useAsyncData's:lazy,server,watch,default,immediateand the rest.transformandpickare not offered: shape the value in the handler.
TanStack Query
With query: true, the module installs
Vue Query.
It gives each request on the server its own QueryClient, and the browser
one. What SSR fetched goes to the browser in the payload. Three composables
are auto-imported:
<script setup lang="ts">
const route = useRoute();
const id = computed(() => Number(route.params.id));
// useQuery of an operation: the input may hold refs, and the query follows them.
const { data: employee, suspense } = useApiQuery('get', '/employees/{id}', {
param: { id },
});
await suspense(); // fetched during SSR
// useMutation of an operation: mutate() takes its input.
const queryClient = useQueryClient();
const queries = useApiQueries();
const rename = useApiMutation('patch', '/employees/{id}', {
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: queries.queryKey('get', '/employees/{id}') }),
});
rename.mutate({ param: { id: id.value }, json: { name: 'Ada' } });
</script>
- A query resolves to the data of a 2xx reply, and fails with a
ReplyStatusErrorfor any other, as@nxgt/httpyz-querydoes. - The options come last:
useQuery's own,enabled,staleTime,placeholderDataand the rest, plusinit, what the call is sent with:useApiQuery('get', path, input, { enabled, init: { headers } }). An operation that takes no input takes the options right after the path. useApiQueries()returns the query options of every operation, foruseQuery,useInfiniteQuery,useQueries, or a key to invalidate:useQuery(queries.queryOptions('get', path, input)).- During SSR, await
suspense(), in<script setup>or inonServerPrefetch: without it the query is left for the browser. staleTimeis 5 s by default, so a query SSR fetched is not fetched again as the page hydrates.query: { staleTime: 0 }restores TanStack's default.- An app with a Vue Query plugin of its own sets
query: { plugin: false }. The composables use whicheverQueryClientis installed; handing the SSR cache to the browser is then that plugin's job.
Middleware
use() adds middleware to the client in place. A plugin that runs after the
module's adds it for every call, on both sides:
// app/plugins/api-auth.ts
export default defineNuxtPlugin({
dependsOn: ['@nxgt/openapi-nuxt'],
setup() {
useApi().use(async (request, next) => {
request.headers.set('x-app', 'shop');
return next(request);
});
},
});
An API elsewhere
With baseUrl, the client calls that URL instead, from the server and the
browser, and server may be left out:
openapi: {
operations: 'generated/operations.ts',
baseUrl: 'https://api.example.com/v1',
},
API
Module options
Under openapi in nuxt.config.ts:
| Option | Type | Default | Description |
|---|---|---|---|
server |
string |
none | the file whose default export is the Hono app, or anything with fetch(request), relative to the app's root. Without it, nothing is served |
operations |
string |
none | the generated operations.ts, relative to the app's root. Without it, there is no useApi() |
prefix |
string |
'/api' |
the path the app is served under, taken off before it answers. api/ is read as /api; / throws |
baseUrl |
string |
the page's origin, under prefix |
the API elsewhere. See An API elsewhere |
client |
ClientOptions |
{} |
validate and decode, as createOpenApiClient takes them |
query |
boolean | QueryOptions |
false |
TanStack Query: useApiQuery(), useApiMutation() and useApiQueries(). See TanStack Query |
ModuleOptions
The type of the options above.
ClientOptions
interface ClientOptions {
readonly validate?: boolean | { readonly request?: boolean; readonly response?: boolean };
readonly decode?: boolean;
}
What the client is created with. decode: false changes the replies' types
too, as it does for createOpenApiClient.
QueryOptions
interface QueryOptions {
plugin?: boolean;
staleTime?: number;
}
query, spelled out. plugin: false leaves Vue Query to the app (default
true). staleTime is the queries' default, in milliseconds, with the
plugin (default 5000).
Composables
useApi
function useApi(): Api;
The client the module's plugin provides, auto-imported: one per request on
the server, one in the browser. Api is createOpenApiClient's client for
your table.
useApiData
function useApiData<T, DefaultT = undefined>(
key: string,
handler: (api: Api, context: ApiDataContext) => Promise<T>,
options?: AsyncDataOptions<Payload<T>, Payload<T>, never[], DefaultT>,
): AsyncData<Payload<T> | DefaultT, NuxtError | undefined>;
// and without the key
useAsyncData with the client, auto-imported. ApiDataContext is
{ signal: AbortSignal }. Payload<T> is a reply without its response,
and any other value as it is. See Data for a page.
useApiQuery
function useApiQuery(method, path, ...input, options?: ApiQueryOptions<Data>): UseQueryReturnType<Data, Error>;
useQuery of the operation at path, with query set. input is what the
client's call takes, each field a value, a ref or a getter
(MaybeRefDeep). Data is the data of its 2xx replies. See
TanStack Query.
useApiMutation
function useApiMutation(method, path, options?: ApiMutationOptions<Variables, Data>): UseMutationReturnType<Data, Error, Variables, unknown>;
useMutation of the operation at path, with query set. mutate() takes
its input.
useApiQueries
function useApiQueries(): OpenApiQueries<…>;
createOpenApiQueries(useApi()), from @nxgt/httpyz-query/openapi:
queryOptions, infiniteQueryOptions, mutationOptions and queryKey.
Server
createHonoApp
function createHonoApp<E extends Env = {}>(options?: HonoOptions<NuxtEnv & E>): Hono<NuxtEnv & E>;
type NuxtEnv = { Bindings: NuxtBindings };
interface NuxtBindings { event: H3Event }
A Hono app whose c.env.event is the request's h3 event, auto-imported in
the server's files, and exported by @nxgt/openapi-nuxt/hono. E adds to
its Env; options are Hono's.
Query
What @nxgt/openapi-nuxt/query exports. The module writes the composables
into the app from it. It also re-exports QueryClient, VueQueryPlugin,
dehydrate, hydrate and DehydratedState from @tanstack/vue-query,
so the app and the module share one copy of Vue Query.
apiQueryComposables
function apiQueryComposables<Ops, Routes, Decoded>(
useApi: () => OpenApiClient<Ops, Routes, Decoded>,
): { useApiQueries; useApiQuery; useApiMutation };
The three composables over the client useApi() returns, for an app that
writes its own.
ApiQueryOptions
type ApiQueryOptions<Data> = Omit<UseQueryOptions<Data>, 'queryKey' | 'queryFn'> & {
readonly init?: MaybeRefOrGetter<OperationInit | undefined>;
};
ApiMutationOptions
type ApiMutationOptions<Variables, Data> = Omit<MutationOptions<Data, Error, Variables>, 'mutationKey' | 'mutationFn'> & {
readonly init?: MutationInit<Variables>;
};
MaybeRefDeep
type MaybeRefDeep<T> = MaybeRefOrGetter<{ [K in keyof T]: MaybeRefDeep<T[K]> }>;
A value, a ref or a getter of it, and so on for each field. A Date or a
function is taken as it is.
Functions
normalizePrefix
function normalizePrefix(prefix: string): string;
The prefix as the module mounts it: one leading slash, no trailing one.
Throws for a prefix that names no path, such as /.
Runtime
@nxgt/openapi-nuxt/runtime holds what the files the module writes call.
It imports neither Nuxt nor h3.
serveUnder
function serveUnder<Env>(app: FetchApp<Env>, prefix: string, request: Request, env?: Env): Response | Promise<Response>;
Answers request with app, the prefix taken off its path. A path outside
the prefix, /apis under /api, reaches it unchanged.
apiHttpOptions
function apiHttpOptions(target: ApiTarget): HttpClientOptions;
The @nxgt/httpyz options for one client: baseUrl when there is one, else
the event's fetch on the server, else the page's origin. Throws with none
of them.
ApiTarget
| Field | Type | Description |
|---|---|---|
prefix |
string |
the path the server is mounted under |
baseUrl |
string |
the API elsewhere |
event |
RequestEventLike |
on the server, the request being rendered |
origin |
string |
in the browser, the page's origin. Default: location.origin |
FetchApp
interface FetchApp<Env = unknown> {
fetch(request: Request, env?: Env): Response | Promise<Response>;
}
What server must export: a Hono app is one.
RequestEventLike
interface RequestEventLike {
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
}
The part of h3's event the client uses: its fetch, which calls Nitro in
process.
toPayload
function toPayload<T>(value: T): Payload<T>;
type Payload<T> = T extends { readonly response: Response } ? Omit<T, 'response'> : T;
value as the SSR payload can carry it: a reply without its Response.
useApiData passes its handler's result through it.
keyedArgs
function keyedArgs(args: readonly unknown[]): [key: string, handler: unknown, options: unknown];
useApiData's arguments, the key first: the one the call gives, or the one
Nuxt's compiler appended. Throws with neither, or without a handler.
Traps
- The app's paths have no prefix. It is served under
/api, but its routes are the spec's,/employees/{id}. A spec whoseserverssays/apifits. - The prefix is the app's. A Nitro route of your own under it, in
server/api/for/api, competes with it. Pick another prefix, or move them into the app. useApi()has no types beforenuxi prepare. They come from the files the module writes into.nuxt/.- Return a reply, or plain data, from
useApiData's handler. Anything else the SSR payload cannot serialize, aResponseor a class of your own, fails the render. - A query whose data the payload cannot carry fails the render. A
binary reply, a
Blob, is one: keep such an operation out of SSR withenabled: import.meta.client, or out of TanStack Query. useApiQueryhas noselectof its own type. Its data is the reply's. For a query whose data is transformed, useuseQuery({ ...queries.queryOptions(...), select }).- With
baseUrl, SSR calls leave the process, and carry none of the incoming request's cookies. Add them with a middleware if the API needs them. - The browser bundle holds the spec's schemas. The client validates with
them.
client: { validate: false, decode: false }stops using them, but the table still imports them.
Related Packages
Ready transactional e-mails for a Maizzle project built with @nxgt/mail-ui and @...
E-mail components for a Maizzle project in the style of @nxgt/material-vue: NxLa...
An i18n plugin for a Maizzle project: ICU catalogues checked at build time, t()...
i18n for a Vue or Nuxt app on @nxgt/i18n's conventions: ICU catalogues checked f...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.3 | ^3.5.0 | MIT | |
| 0.3.2 | ^3.5.0 | MIT | |
| 0.3.1 | ^3.5.0 | MIT | |
| 0.3.0 | ^3.5.0 | MIT | |
| 0.2.1 | ^3.0 | MIT | |
| 0.2.0 | ^3.0 | MIT | |
| 0.1.0 | ^3.0 | MIT |