Formularios y Mutaciones

Los formularios permiten crear y actualizar datos en aplicaciones web. Next.js proporciona 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 App Router y usar Acciones de Servidor (Server Actions) para manejar envíos de formularios y mutaciones de datos. Las Acciones de Servidor 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 son solo del mismo origen por defecto.
  • 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

Formulario solo en servidor

Con el Pages Router, necesitas crear manualmente endpoints de API para manejar de forma segura la mutación 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,
    })

    // Manejar 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,
    })

    // Manejar 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>
  )
}

Validación de formularios

Recomendamos usar validación HTML como required y type="email" para validación básica en el cliente.

Para validación más avanzada en el servidor, puedes usar una biblioteca de validación de esquemas como zod para validar los campos del formulario antes de mutar los datos:

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)
  // ...
}

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('Error al enviar los datos. Por favor, inténtalo de nuevo.')
      }

      // 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 ? 'Cargando...' : 'Enviar'}
        </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('Error al enviar los datos. Por favor, inténtalo de nuevo.')
      }

      // 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 ? 'Cargando...' : 'Enviar'}
        </button>
      </form>
    </div>
  )
}

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) // Establecer carga a true cuando comienza la solicitud

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

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

  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) // Establecer carga a true cuando comienza la solicitud

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

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

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

Redireccionamiento

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}`)
}

Configuración de cookies

Puedes establecer cookies dentro de una Ruta de 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('La cookie ha sido establecida.')
}
export default async function handler(req, res) {
  res.setHeader('Set-Cookie', 'username=lee; Path=/; HttpOnly')
  res.status(200).send('La cookie ha sido establecida.')
}

Lectura de cookies

Puedes leer cookies dentro de una Ruta de API usando el ayudante 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 de 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('La cookie ha sido eliminada.')
}
export default async function handler(req, res) {
  res.setHeader('Set-Cookie', 'username=; Path=/; HttpOnly; Max-Age=0')
  res.status(200).send('La cookie ha sido eliminada.')
}