E2E Type Inference
New in v0.3.0
End-to-end type inference eliminates manual generics. Define your action once on the server, and the client automatically knows the input and output types.
Overview
Instead of writing:
// Manual generics — tedious and error-prone
const { execute, data } = useAction<{ title: string }, { id: number; title: string }>('/api/todos', {
method: 'POST',
})You can now write:
import { createTodo } from '#actions'
// Types fully auto-inferred!
const { execute, data } = useAction(createTodo, {
onSuccess(data) {
// data: { id: number; title: string } — inferred automatically
},
})
await execute({ title: 'Buy milk' }) // input: { title: string } — inferredHow It Works
1. Create Actions in server/actions/
Place your action files in the server/actions/ directory:
server/
actions/
create-todo.ts # POST /api/_actions/create-todo
list-todos.get.ts # GET /api/_actions/list-todos
update-todo.put.ts # PUT /api/_actions/update-todo
delete-todo.delete.ts # DELETE /api/_actions/delete-todo2. Define Actions with defineAction
// server/actions/create-todo.ts
import { z } from 'zod'
export default defineAction({
input: z.object({
title: z.string().min(1),
}),
handler: async ({ input }) => {
// Save to database...
return { id: Date.now(), title: input.title, done: false }
},
})3. Import and Use with Full Types
<script setup lang="ts">
import { createTodo } from '#actions'
const { execute, data, isExecuting, hasSucceeded } = useAction(createTodo, {
onSuccess(todo) {
// todo is typed as { id: number; title: string; done: boolean }
console.log('Created:', todo.title)
},
})
</script>
<template>
<button @click="execute({ title: 'New todo' })" :disabled="isExecuting">
{{ isExecuting ? 'Creating...' : 'Create Todo' }}
</button>
<p v-if="hasSucceeded">Created: {{ data?.title }}</p>
</template>HTTP Method Convention
The file name determines the HTTP method:
| File Name | HTTP Method | Route |
|---|---|---|
create-todo.ts | POST (default) | /api/_actions/create-todo |
list-todos.get.ts | GET | /api/_actions/list-todos |
update-todo.put.ts | PUT | /api/_actions/update-todo |
update-todo.patch.ts | PATCH | /api/_actions/update-todo |
delete-todo.delete.ts | DELETE | /api/_actions/delete-todo |
Generated Exports
The #actions virtual module exports a camelCase reference for each action file:
| File Name | Export Name |
|---|---|
create-todo.ts | createTodo |
list-todos.get.ts | listTodos |
update-user.put.ts | updateUser |
Files in subdirectories get a prefixed flat name: auth/login.post.ts exports authLogin and is served at /api/_actions/auth/login.
Grouped actions Namespace
Alongside the flat exports, #actions also exports one grouped actions object that mirrors your directory structure -- handy for discoverability and autocomplete in larger apps:
server/actions/
create-todo.ts
auth/
login.post.ts
register.post.tsimport { actions } from '#actions'
useAction(actions.createTodo) // root files sit at the top level
useAction(actions.auth.login) // identical reference to the flat authLoginBoth forms are the same frozen typed reference -- pick whichever reads better. Name collisions between a directory and an action file are reported as a build warning, and the conflicting entry is skipped in the grouped object (the flat export always works). One special case: an action file named actions.ts would shadow the grouped export itself, so the grouped object is omitted entirely (with a build warning) until the file is renamed.
Configuration
You can customize the actions directory in your nuxt.config.ts:
export default defineNuxtConfig({
actions: {
actionsDir: 'actions', // default: 'actions' (relative to server/)
},
})Using with useOptimisticAction
Typed references work with optimistic actions too:
<script setup lang="ts">
import { toggleTodo } from '#actions'
const todos = ref([{ id: 1, title: 'Buy milk', done: false }])
const { execute, optimisticData } = useOptimisticAction(toggleTodo, {
currentData: todos,
updateFn: (input, current) =>
current.map(t => t.id === input.id ? { ...t, done: !t.done } : t),
})
</script>Backward Compatibility
The string-path API still works exactly as before:
// This still works — no changes needed for existing code
const { execute } = useAction<{ title: string }, Todo>('/api/todos', {
method: 'POST',
})Both patterns can coexist in the same project. Use #actions for new code and migrate existing code at your own pace.