Formularios y mutaciones

Los formularios te permiten crear y actualizar datos en aplicaciones web. Next.js ofrece una forma poderosa de manejar envíos de formularios y mutaciones de datos usando Rutas de API.

Es bueno saber:

  • Pronto recomendaremos adoptar incrementalmente el Enrutador de Aplicación (App Router) y usar Acciones de Servidor (Server Actions) para manejar envíos de formularios y mutaciones de datos. Las Acciones de Servidor te permiten definir funciones asíncronas en el servidor que pueden ser llamadas directamente desde tus componentes, sin necesidad de crear manualmente una Ruta de API.
  • Las Rutas de API no especifican cabeceras CORS, lo que significa que por defecto solo permiten solicitudes del mismo origen.
  • Dado que las Rutas de API se ejecutan en el servidor, podemos usar valores sensibles (como claves de API) a través de Variables de Entorno sin exponerlos al cliente. Esto es crítico para la seguridad de tu aplicación.

Ejemplos

Formularios Exclusivos del Servidor

Con el Enrutador de Páginas, necesitas crear manualmente endpoints de API para manejar de forma segura mutaciones de datos en el servidor.

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const data = req.body
  const id = await createItem(data)
  res.status(200).json({ id })
}
export default function handler(req, res) {
  const data = req.body
  const id = await createItem(data)
  res.status(200).json({ id })
}

Luego, llama a la Ruta de API desde el cliente con un manejador de eventos:

import { FormEvent } from 'react'

export default function Page() {
  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    })

    // Maneja la respuesta si es necesario
    const data = await response.json()
    // ...
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Enviar</button>
    </form>
  )
}
export default function Page() {
  async function onSubmit(event) {
    event.preventDefault()

    const formData = new FormData(event.target)
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    })

    // Maneja la respuesta si es necesario
    const data = await response.json()
    // ...
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit">Enviar</button>
    </form>
  )
}

Redireccionando

Si deseas redirigir al usuario a una ruta diferente después de una mutación, puedes usar redirect a cualquier URL absoluta o relativa:

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const id = await addPost()
  res.redirect(307, `/post/${id}`)
}
export default async function handler(req, res) {
  const id = await addPost()
  res.redirect(307, `/post/${id}`)
}

Validación de Formularios

Recomendamos usar validación HTML como required y type="email" para validación básica de formularios.

Para validación más avanzada en el servidor, usa una biblioteca de validación de esquemas como zod para validar la estructura de los datos del formulario parseados:

import type { NextApiRequest, NextApiResponse } from 'next'
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const parsed = schema.parse(req.body)
  // ...
}
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function handler(req, res) {
  const parsed = schema.parse(req.body)
  // ...
}

Mostrando Estado de Carga

Puedes usar el estado de React para mostrar un estado de carga cuando un formulario se está enviando al servidor:

import React, { useState, FormEvent } from 'react'

export default function Page() {
  const [isLoading, setIsLoading] = useState<boolean>(false)

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setIsLoading(true) // Establece carga a true cuando comienza la solicitud

    try {
      const formData = new FormData(event.currentTarget)
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      })

      // Maneja la respuesta si es necesario
      const data = await response.json()
      // ...
    } catch (error) {
      // Maneja el error si es necesario
      console.error(error)
    } finally {
      setIsLoading(false) // Establece carga a false cuando completa la solicitud
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Cargando...' : 'Enviar'}
      </button>
    </form>
  )
}
import React, { useState } from 'react'

export default function Page() {
  const [isLoading, setIsLoading] = useState(false)

  async function onSubmit(event) {
    event.preventDefault()
    setIsLoading(true) // Establece carga a true cuando comienza la solicitud

    try {
      const formData = new FormData(event.currentTarget)
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      })

      // Maneja la respuesta si es necesario
      const data = await response.json()
      // ...
    } catch (error) {
      // Maneja el error si es necesario
      console.error(error)
    } finally {
      setIsLoading(false) // Establece carga a false cuando completa la solicitud
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="name" />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Cargando...' : 'Enviar'}
      </button>
    </form>
  )
}

Manejo de Errores

Puedes usar el estado de React para mostrar un mensaje de error cuando falla el envío de un formulario:

import React, { useState, FormEvent } from 'react'

export default function Page() {
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [error, setError] = useState<string | null>(null)

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setIsLoading(true)
    setError(null) // Limpiar errores previos cuando comienza una nueva solicitud

    try {
      const formData = new FormData(event.currentTarget)
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      })

      if (!response.ok) {
        throw new Error('Failed to submit the data. Please try again.')
      }

      // Manejar la respuesta si es necesario
      const data = await response.json()
      // ...
    } catch (error) {
      // Capturar el mensaje de error para mostrar al usuario
      setError(error.message)
      console.error(error)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div>
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <form onSubmit={onSubmit}>
        <input type="text" name="name" />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Submit'}
        </button>
      </form>
    </div>
  )
}
import React, { useState } from 'react'

export default function Page() {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)

  async function onSubmit(event) {
    event.preventDefault()
    setIsLoading(true)
    setError(null) // Limpiar errores previos cuando comienza una nueva solicitud

    try {
      const formData = new FormData(event.currentTarget)
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
      })

      if (!response.ok) {
        throw new Error('Failed to submit the data. Please try again.')
      }

      // Manejar la respuesta si es necesario
      const data = await response.json()
      // ...
    } catch (error) {
      // Capturar el mensaje de error para mostrar al usuario
      setError(error.message)
      console.error(error)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <div>
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <form onSubmit={onSubmit}>
        <input type="text" name="name" />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Submit'}
        </button>
      </form>
    </div>
  )
}

Configuración de Cookies

Puedes configurar cookies dentro de una Ruta API usando el método setHeader en la respuesta:

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.setHeader('Set-Cookie', 'username=lee; Path=/; HttpOnly')
  res.status(200).send('Cookie has been set.')
}
export default async function handler(req, res) {
  res.setHeader('Set-Cookie', 'username=lee; Path=/; HttpOnly')
  res.status(200).send('Cookie has been set.')
}

Lectura de Cookies

Puedes leer cookies dentro de una Ruta API usando el helper de solicitud cookies:

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const auth = req.cookies.authorization
  // ...
}
export default async function handler(req, res) {
  const auth = req.cookies.authorization
  // ...
}

Eliminación de Cookies

Puedes eliminar cookies dentro de una Ruta API usando el método setHeader en la respuesta:

import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.setHeader('Set-Cookie', 'username=; Path=/; HttpOnly; Max-Age=0')
  res.status(200).send('Cookie has been deleted.')
}
export default async function handler(req, res) {
  res.setHeader('Set-Cookie', 'username=; Path=/; HttpOnly; Max-Age=0')
  res.status(200).send('Cookie has been deleted.')
}