usage

Form Actions

Use form actions to submit data to the server.

Form Actions are an alternative to api routes, and useFetch/useAsyncData composables. They allow you to submit data to your server using native HTML forms that can be progressively enhanced.

Basic Usage

Let's start by creating a Nuxt page in pages/login.vue :

pages/login.vue
<template>
  <form method="POST">
    <label>
      Email
      <input name="email" type="email" autocomplete="username">
    </label>
    <label>
      Password
      <input name="password" type="password" autocomplete="current-password">
    </label>
    <button>Log in</button>
  </form>
</template>

If you navigate to /login and you click the button on a form like this, the browser will send a POST request to the current path, which is /login.

Note: The path matched by your page is very important, as it respects the default behaviour of the browser.

In order to handle such a request with Nuxt, you need to create a route handler. With this module, you can create a form action to handle this using the /server/actions directory. Let's create an action in /server/actions/login.ts :

/server/actions/login.ts
export default defineFormActions({
  default: () => {
    console.log("Login called !")
  }
})

Note: It's important that the file name matches the path of the page, and that you use a default export with the defineFormActions composable.

defineFormActions accepts an object of h3 event handlers. The key that you use only matters if you want to handle more than 1 action on the same route. By convention we use default for the main action.

You have now a working form action, but it doesn't do much. Let's add some logic to it.

Handling form data

Continuing with the previous example, let's add some dummy logic to our form action :

/server/actions/login.ts
// Replace with real logic
const createSession = (user: unknown) => "session-id"
// Replace with real logic
const getUser = (email: string, password: string) => ({ name: "Luke" })
// Replace with real validation
const validValue = (v: unknown): v is string => typeof v === "string" && v.length > 0
export default defineFormActions({
  signIn: async (event) => {
    // h3 exports a readFormData to obtain a FormData object
    const formData = await readFormData(event)
    const email = formData.get("email")
    const password = formData.get("password")
    // Handle your errors
    if (!validValue(email)) {
      return actionResponse(event, { email, invalid: true },
        { error: { message: "Invalid email" } })
    }
    if (!validValue(password)) {
      return actionResponse(event, { email, invalid: true },
        { error: { message: "Invalid password" } })
    }
    // Load the user
    const user = getUser(email, password)
    if (!user) {
      return actionResponse(event, { email, incorrect: true },
        { error: { message: "No user found" } })
    }
    // Attach a session cookie to the response
    setCookie(event, "session", createSession(user))
    // Respond with the user
    return actionResponse(event, { user })
  }
})

Now on succesful submissions, our server route will respond with a JSON payload containing the user data, and a session cookie.

Server composables defineFormActions and actionResponse are auto-imported, but you can explicitly import them from #form-actions.

Progressively enhancing the form

Now that we have a working form action, we can progressively enhance the form to use it.

The useFormAction composable expose multiple helpers to help you with this.

  • enhance must be bound to the form element with the custom v-enhance directive.
  • data is a reactive object that will contain the response from the form action.

We can now use vue to display the response from the form action and to handle the error states. We can also bind the value of the inputs to the response from the form action, so that the form is pre-filled with the values that were submitted in case of error.

/pages/login.vue
<script setup lang="ts">
const { enhance, data } = await useFormAction()
</script>
<template>
  <form v-enhance="enhance" method="POST" action="login">
    <p v-if="data.formResponse?.invalid" class="error">
      Invalid credentials.
    </p>
    <p v-if="data.formResponse?.incorrect" class="error">
      User does not exists.
    </p>
    <p v-if="data.formResponse?.user" class="success">
      {{ data.formResponse.user.name }} Found !
    </p>
    <label>
      Email
      <input name="email" type="email" :value="data.formResponse?.email ?? ''">
    </label>
    <label>
      Password
      <input name="password" type="password">
    </label>
    <button>Log in</button>
  </form>
</template>

Redirecting

It's common to redirect the user after a successful form submission. Let's first create a profile page to redirect our users to :

pages/profile.vue
<template>
  <h1>Profile</h1>
</template>

Now let's update our form action to redirect to this page. You can do this by using the 3rd argument of actionResponse :

/server/actions/login.ts
export default defineFormActions({
  signIn: (event) => {
    // ...
    return actionResponse(event, { user }, { redirect: "/profile" })
  }
})

By default Nuxt will use server side navigation and hard navigate to /profile. However, if your form is progressively enhanced, Nuxt will use client side navigation instead.

Multiple actions

It's possible that you want to handle several actions in the same form actions. defineFormActions let you define multiple actions, and you can use the a query parameter to specify which action to call.

Let's add some actions to our profile page :

pages/profile.vue
<template>
  <h1>Profile</h1>
  <form method="POST">
    <button>Log out</button>
    <button formaction="profile?delete">
      Delete account
    </button>
  </form>
</template>
If you do not specify a formaction attribute on your button, the default action will be matched first. If no action is found, the first action will be called.

We need 2 event handlers to handle these actions. Let's create a route handler for these 2 actions :

/server/actions/profile.ts
export default defineFormActions({
  logout: () => {
    console.log("logout ...")
  },
  delete: () => {
    console.log("delete ...")
  }
})

Now when we click on the buttons a POST request will be sent to /profile or /profile?delete. The route handler will execute the correct matching handler.

Example

Refer to the simple template to see a full setup.