An advanced Nuxt 3/4 module that automatically generates fully-typed API composables for all your server endpoints.
Stop writing manual API calls — No more useFetch('/api/posts/42') scattered throughout your app with zero type safety.
// pages/users.vue ❌
const { data } = await useFetch('/api/users', { 
  method: 'POST', 
  body: { name: 'John', email: 'john@example.com' } 
}) // No autocomplete, no type checking, URL typos break silently
// pages/posts.vue ❌  
const { data } = await useFetch(`/api/posts/${postId}`) // Manual string building
// pages/orders.vue ❌
const { data } = await useFetch('/api/orders', {
  method: 'PUT',
  body: orderData
}) // Hope the payload structure is correct 🤞With nuxt-apex, every endpoint becomes a typed composable:
// All auto-generated, fully typed, aliase supported, with autocomplete ✅
const post = useTFetchPostsGetById({ id: postId })
const users = await useTFetchUsersPostAsync({ name: 'John', email: 'john@example.com' })
const order = await useTFetchOrdersPutAsync(orderData)
// or can be aliased like (see Configuration section for more info)
const post = getPost({ id: postId })
const users = await addUsers({ name: 'John', email: 'john@example.com' })
const order = await updateOrder(orderData)Works with any API complexity — Simple CRUD, complex business logic, authentication, middleware, error handling. If you can define it with defineApexHandler, you get a typed composable.
- Zero Boilerplate — Write your API once, get typed composables everywhere.
- Always in Sync — Change your API types, composables update automatically.
- Full Type Safety — Catch API contract mismatches at compile time, not runtime.
npm install nuxt-apexAdd to your nuxt.config.ts:
export default defineNuxtConfig({
  modules: ['nuxt-apex']
})Start your dev server and nuxt-apex will automatically scan your server/api/ directory:
npm run devFile: server/api/posts/[id].get.ts (follows Nuxt's file-based routing)
// Your current Nuxt API route (still works!)
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  return { id: Number(id), title: 'Amazing title' }
})
// Just change defineEventHandler → defineApexHandler + add types
interface Input {
  id: number
}
export default defineApexHandler<Input>(async (data, event) => {
  return { id: data.id, title: 'Amazing title' }  // Now fully typed!
})File: pages/posts.vue
// Auto-fetching composable (runs immediately)
const { data, error, pending } = useTFetchPostsGetById({ id: 42 })
// Async version for on-demand usage (button clicks, form submissions, etc.)
async function handleClick() {
  const data = await useTFetchPostsGetByIdAsync({ id: 42 })
  // Handle the response...
}import { z } from 'zod'
export default defineApexHandler<Input>(async (data, event) => {
  return { id: data.id, title: 'Amazing title' }
}, {
  id: z.coerce.number().int().positive().min(1)  // Zod validation
})
// or use callback style with z instance provided (second arg is `data` object for more precise validation)
export default defineApexHandler<Input>(async (data, event) => {
  return { id: data.id, title: 'Amazing title' }
}, (z, d) => ({
  id: z.coerce.number().int().positive().min(1)  // Zod validation
}))export default defineNuxtConfig({
  modules: ['nuxt-apex'],
  apex: {
    sourcePath: 'api',                              // API source folder
    outputPath: 'composables/.nuxt-apex',           // Output for composables
    cacheFolder: 'node_modules/.cache/nuxt-apex',   // Output for cache
    composablePrefix: 'useTFetch',                  // Composable prefix
    namingFunction: undefined,                      // Custom naming function
    listenFileDependenciesChanges: true,            // Watch for file changes
    serverEventHandlerName: 'defineApexHandler',    // Server event handler name
    tsConfigFilePath: undefined,                    // Path to tsconfig.json
    ignore: ['api/internal/**'],                    // Patterns to ignore
    concurrency: 50,                                // Concurrency limit
    tsMorphOptions: { /* ... */ },                  // ts-morph options
  }
})Custom Naming Function: If you need more control over composable names, provide a custom naming function & composablePrefix:
apex: {
  composablePrefix: 'useApi',
  namingFunction: (path: string) => {
    const method = ...
    return `${path.split('/').map(capitalize).join('')}${capitalize(method)}`
    // Result: useApiPostsIdGet instead of useTFetchPostsGetById
  }
}Aliases: If you need to alias a composable, provide it on top of the defineApexHandler:
interface Input {
  id: number
}
// as: getPosts      <--- like this
/* as: getPosts */   <--- or like this
/**                  <--- or like this
* @alias getPosts
*/
export default defineApexHandler<Input>(async (data, event) => {
  return { id: data.id, title: 'Amazing title' }
})Now in the client call getPosts instead of useTFetchPostsGetById:
const { data, error, pending } = getPosts({ id: 42 })You can still use the original useTFetchPostsGetById if you need to.
nuxt-apex generates two versions of each composable:
1. Auto-fetching version (use in setup):
// Runs immediately when component mounts
const { data, error, pending, refresh } = useTFetchPostsGetById({ id: 42 })2. Async version (use for on-demand calls):
// Perfect for button clicks, form submissions, conditional fetching
async function submitOrder() {
  try {
    const result = await useTFetchOrdersPostAsync(orderData)
    // Handle success
  } catch (error) {
    // Handle error
  }
}All composables accept an optional second argument with the same options as Nuxt's useFetch:
const { data, pending, error, execute } = useTFetchPostsGetById(
  { id: 42 }, 
  { 
    immediate: false,    // Don't fetch automatically
    watch: false,        // Don't watch for parameter changes
    server: false,       // Skip server-side rendering
    lazy: true,          // Don't block navigation
    // ...all other useFetch options work here
  }
)composables/ directory (not .nuxt or node_modules) to work properly with Nuxt's auto-import system.
Why this matters:
Nuxt has special behavior for the composables/ folder:
- Only files in composables/(or subfolders) are automatically registered as true composables
- Files outside this folder are treated as regular utilities and lose Nuxt context
- This means they can't access SSR context, plugins, or other Nuxt runtime features
What happens if you put them elsewhere:
// ❌ If composables are in .nuxt/ or node_modules/
const data = useTFetchPostsGetById({ id: 42 })
// Error: "useFetch can only be used within a Nuxt app setup function"The fix is simple - just ensure your outputPath points to somewhere inside composables/:
// nuxt.config.ts ✅
apex: {
  outputPath: 'composables/.nuxt-apex', // Inside composables/ - works perfectly
  // outputPath: '.nuxt/apex',          // ❌ Outside composables/ - breaks
}Default behavior: nuxt-apex automatically uses composables/.nuxt-apex as the output path, so this works out of the box. Only change it if you need a custom structure.
nuxt-apex uses caching to speed up composable generation. You have two options:
Option 1: Default (node_modules cache)
apex: {
  cacheFolder: 'node_modules/.cache/nuxt-apex' // Default
}Pros:
- ✅ Keeps your project clean - cache files don't clutter your source code
- ✅ Gitignored by default - no accidental commits of cache files
- ✅ Standard location that tools expect
Cons:
- ❌ Cache is lost when node_modulesis deleted or withgit pullconflicts
- ❌ Slower regeneration after fresh installs
Option 2: Local cache (synced with git)
apex: {
  cacheFolder: 'composables/.nuxt-apex/'
}Pros:
- ✅ Cache survives node_modulesdeletion
- ✅ Faster setup for new team members (cache comes with git clone)
- ✅ More predictable builds across environments
Cons:
- ❌ Cache files are committed to your repository
- ❌ Larger git repository size
- ❌ Potential merge conflicts in cache files
Recommendation: Use the default unless you have a large API and slow generation times, or your team frequently deletes node_modules.
Default behavior: nuxt-apex automatically uses composables/.nuxt-apex as the output path, so this works out of the box. Only change it if you need a custom structure.