12k
All articles

Cómo Construir una API CRUD con AdonisJS

Crea una API CRUD con AdonisJS v7: rutas de posts, modelos Lucid, validación VineJS y respuestas JSON probadas con curl.

OpenReplay Team
OpenReplay Team
Cómo Construir una API CRUD con AdonisJS

AdonisJS es un framework de Node.js inspirado en Laravel y orientado a TypeScript que integra enrutamiento, un ORM (Lucid), validación (VineJS), autenticación y una CLI (ace) en un único paquete — lo que lo convierte en una opción sólida para APIs CRUD cuando se prefieren convenciones en lugar de configuración manual. Este tutorial construye un endpoint CRUD completo para posts con AdonisJS v7, cubriendo cada paso desde el andamiaje hasta una solicitud curl probada, incluyendo validación con VineJS y respuestas de error estructuradas.

AdonisJS v7 difiere de v6 en tres aspectos que este tutorial refleja: la configuración (Node.js 24+, npm create adonisjs@latest); el starter de API (un monorepo con Turborepo que aloja el backend en apps/backend/, con Lucid ORM y autenticación preconfigurados); y los patrones de código (enrutamiento a través de #generated/controllers, modelos Lucid que extienden clases de esquema generadas desde apps/backend/database/schema.ts). Los ejemplos a continuación siguen esa estructura de v7 en todo momento.

Puntos Clave

  • El starter de API de AdonisJS v7 se genera con npm create adonisjs@latest my-api -- --kit=api, no con la forma antigua de v6 npm init adonisjs@latest ... -- -K=api.
  • AdonisJS v7 requiere Node.js 24+ y npm 11+, por lo que debes actualizar tu entorno de ejecución local antes de generar un proyecto.
  • Los nuevos starter kits de v7 incluyen Lucid ORM, SQLite, autenticación, VineJS y configuración de pruebas de forma predeterminada, por lo que no es necesario agregar Lucid manualmente en un starter de API nuevo.
  • Los ejemplos de enrutamiento actuales de v7 utilizan la importación del barrel generado para controladores, import { controllers } from '#generated/controllers', y vinculan rutas con manejadores del estilo [controllers.Posts, 'index'].
  • Los fallos de validación de VineJS en solicitudes a la API devuelven HTTP 422 con un array estructurado errors que contiene nombres de campos, reglas fallidas y mensajes — la forma que las bibliotecas de formularios del frontend esperan para mostrar errores a nivel de campo.
  • El método Post.findOrFail(id) de Lucid lanza una excepción de recurso no encontrado que AdonisJS convierte automáticamente en una respuesta JSON 404, sin necesidad de verificar manualmente si el valor es nulo.

Requisitos Previos

AdonisJS v7 requiere Node.js 24 o posterior y npm 11 o posterior. Verifica las versiones locales antes de generar el proyecto:

node --version   # debe ser >= 24.x
npm --version    # debe ser >= 11.x

Los starter kits de v7 incluyen Lucid configurado con SQLite de forma predeterminada, por lo que no es necesario comenzar con PostgreSQL para completar este tutorial. SQLite escribe en un archivo local y es ideal para un tutorial CRUD desechable. Si más adelante migras a PostgreSQL, MySQL u otra base de datos SQL, Lucid sigue siendo compatible con esos controladores; el código del controlador y de validación que se muestra a continuación permanece igual.

Generar el Proyecto con el Starter Kit de API

El starter de API de AdonisJS v7 se genera con npm create adonisjs@latest <nombre> -- --kit=api. Ejecútalo ahora:

npm create adonisjs@latest my-api -- --kit=api

El flag --kit=api selecciona el starter kit de API. Proporciona un proyecto orientado al backend con valores predeterminados amigables para APIs, incluyendo Lucid ORM, validación con VineJS, autenticación, CORS, pruebas y una base de datos SQLite por defecto.

Accede al directorio del proyecto e inicia el servidor de desarrollo:

cd my-api
npm run dev

El backend se ejecuta en http://localhost:3333 de forma predeterminada. El starter de API también incluye endpoints de autenticación bajo /api/v1/auth, pero este tutorial mantiene los endpoints de posts como públicos para que puedas concentrarte primero en el CRUD.

Usar la Configuración Predeterminada de Lucid

Lucid es el ORM SQL de AdonisJS. En tutoriales más antiguos, con frecuencia verás un paso de configuración manual como este:

node ace add @adonisjs/lucid

No ejecutes ese comando en un starter de API v7 nuevo. Lucid ya está instalado y configurado, y el starter utiliza SQLite de forma predeterminada. Puedes pasar directamente a crear una tabla.

Generar una Migración

Una migración es un cambio de esquema versionado. Genera una migración para la tabla posts:

node ace make:migration posts

El comando escribe un archivo con marca de tiempo en database/migrations/. Ábrelo y define las columnas de la tabla:

import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'posts'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('title').notNullable()
      table.text('body').notNullable()
      table.timestamp('created_at')
      table.timestamp('updated_at')
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

La ruta de importación sigue siendo @adonisjs/lucid/schema. El cambio importante de v7 llega después de ejecutar la migración: Lucid genera clases de esquema a partir de tu base de datos para que el modelo de tu aplicación pueda heredar definiciones de columnas tipadas.

Ejecutar la Migración y Crear el Modelo

Aplica el esquema a la base de datos:

node ace migration:run

Lucid crea la tabla posts y genera database/schema.ts. Ese archivo generado contiene una clase PostsSchema con definiciones de columnas tipadas para la tabla. No edites database/schema.ts directamente; se regenera después de cada migración.

Ahora crea el modelo:

node ace make:model Post

Abre app/models/post.ts y haz que el modelo extienda la clase de esquema generada exportada desde database/schema.ts:

import { PostSchema } from '#database/schema'

export default class Post extends PostSchema {}

Este es el patrón de modelo Lucid en v7. Los decoradores de columnas residen en la clase de esquema generada, mientras que el modelo de tu aplicación es donde más adelante agregarás relaciones, hooks, scopes de consulta y métodos personalizados.

Generar un Controlador

node ace make:controller posts --resource genera un controlador con recursos para el recurso posts. Ejecútalo ahora:

node ace make:controller posts --resource

El flag --resource genera los métodos REST convencionales: index, create, store, show, edit, update y destroy. Para una API JSON, solo necesitas cinco de ellos:

MétodoPropósito
indexListar todos los posts
storeCrear un nuevo post a partir del cuerpo de la solicitud
showDevolver un post individual por id
updateModificar un post existente por id
destroyEliminar un post por id

Puedes eliminar los métodos create y edit generados, ya que existen para formularios HTML renderizados en el servidor.

Configurar las Rutas

La documentación actual de v7 muestra los controladores importados a través del archivo barrel generado, #generated/controllers. Abre start/routes.ts y agrega las rutas de posts:

import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'

router
  .group(() => {
    router.get('/posts', [controllers.Posts, 'index'])
    router.post('/posts', [controllers.Posts, 'store'])
    router.get('/posts/:id', [controllers.Posts, 'show'])
    router.route('/posts/:id', ['PUT', 'PATCH'], [controllers.Posts, 'update'])
    router.delete('/posts/:id', [controllers.Posts, 'destroy'])
  })
  .prefix('/api/v1')

Esto registra los endpoints de la API bajo /api/v1/posts, siguiendo el estilo de API versionada utilizado por el starter de API v7.

MétodoRutaMétodo del controladorDescripción
GET/api/v1/postsindexListar todos los posts
POST/api/v1/postsstoreCrear un post
GET/api/v1/posts/:idshowObtener un post
PUT/api/v1/posts/:idupdateActualizar un post
PATCH/api/v1/posts/:idupdateActualizar parcialmente un post
DELETE/api/v1/posts/:iddestroyEliminar un post

AdonisJS sigue documentando router.resource() para rutas RESTful, pero una ruta de recurso simple registra siete rutas, incluyendo GET /posts/create y GET /posts/:id/edit. La lista explícita de rutas anterior mantiene la superficie de la API exclusivamente JSON y evita exponer accidentalmente rutas para mostrar formularios.

Verifica las rutas activas con:

node ace list:routes

Si las rutas no aparecen, reinicia el servidor de desarrollo para que el archivo barrel del controlador generado se actualice.

Agregar Validación con VineJS

VineJS es la biblioteca de validación utilizada por AdonisJS. Genera un archivo de validador:

node ace make:validator post

Abre app/validators/post.ts y define los validadores para crear y actualizar posts:

import vine from '@vinejs/vine'

export const createPostValidator = vine.create({
  title: vine.string().trim().minLength(3),
  body: vine.string().trim().minLength(1),
})

export const updatePostValidator = vine.create({
  title: vine.string().trim().minLength(3).optional(),
  body: vine.string().trim().minLength(1).optional(),
})

La guía de validación actual de AdonisJS utiliza vine.create() para definir validadores. En los controladores, request.validateUsing() ejecuta el validador contra el cuerpo de la solicitud y devuelve el payload validado y tipado.

Cuando request.validateUsing() falla en una solicitud a la API, AdonisJS devuelve HTTP 422 con un array JSON estructurado errors. Cada error incluye el nombre del campo, la regla fallida y un mensaje. Esa es la forma que el código del frontend puede mapear directamente al estado de los campos del formulario.

Las repeticiones de sesión de frontends que consumen APIs CRUD revelan con frecuencia un modo de fallo específico: un formulario se envía, el servidor devuelve un 422, pero ningún mensaje de error a nivel de campo aparece en la interfaz. La causa raíz suele ser que el servidor devolvió una cadena de texto libre en lugar de errores de campo estructurados. VineJS proporciona la forma estructurada de forma predeterminada, así que consérvala. No captures la excepción de validación y la relances como una cadena plana, a menos que también preserves el contrato a nivel de campo.

Implementar los Métodos del Controlador

Cada acción del controlador es un manejador conciso. Los dos casos de error que importan — un registro inexistente y una entrada inválida — son gestionados por Lucid y VineJS sin ramificaciones manuales.

Abre app/controllers/posts_controller.ts y reemplaza su contenido:

import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import { createPostValidator, updatePostValidator } from '#validators/post'

export default class PostsController {
  async index({ response }: HttpContext) {
    const posts = await Post.all()
    return response.json(posts)
  }

  async store({ request, response }: HttpContext) {
    const payload = await request.validateUsing(createPostValidator)
    const post = await Post.create(payload)
    return response.created(post)
  }

  async show({ params, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    return response.json(post)
  }

  async update({ params, request, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    const payload = await request.validateUsing(updatePostValidator)

    await post.merge(payload).save()

    return response.json(post)
  }

  async destroy({ params, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    await post.delete()

    return response.noContent()
  }
}

Algunos detalles importantes a tener en cuenta:

  • store devuelve response.created(post), lo que establece HTTP 201.
  • destroy devuelve response.noContent(), lo que establece HTTP 204.
  • await post.delete() se espera con await, de modo que la respuesta no se devuelve antes de que la eliminación finalice.
  • Post.findOrFail(params.id) lanza una excepción cuando no existe ninguna fila coincidente. AdonisJS convierte esa excepción automáticamente en una respuesta 404.

Probar con curl

Una API CRUD de AdonisJS v7 puede ejercitarse de extremo a extremo con curl — cada acción de recurso (index, store, show, update, destroy) se corresponde con una única solicitud HTTP bajo /api/v1/posts. Con npm run dev en ejecución, crea un post:

curl -X POST http://localhost:3333/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "First post", "body": "Hello from Adonis"}'

Una respuesta exitosa de store devuelve HTTP 201 con el registro creado:

{
  "title": "First post",
  "body": "Hello from Adonis",
  "createdAt": "2026-06-01T12:00:00.000+00:00",
  "updatedAt": "2026-06-01T12:00:00.000+00:00",
  "id": 1
}

Ahora provoca errores de validación enviando un título demasiado corto y un cuerpo vacío:

curl -X POST http://localhost:3333/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "ok", "body": ""}'

La respuesta es HTTP 422 con un array de errores estructurado de VineJS:

{
  "errors": [
    {
      "field": "title",
      "rule": "minLength",
      "message": "The title field must have at least 3 characters"
    },
    {
      "field": "body",
      "rule": "minLength",
      "message": "The body field must have at least 1 characters"
    }
  ]
}

Este es el contrato que el frontend mapea al estado del formulario: itera sobre errors, indexa por field y muestra message junto al campo correspondiente.

Solicita un registro que no existe para ver el flujo del 404:

curl -i http://localhost:3333/api/v1/posts/9999

findOrFail produce una respuesta 404 en lugar de devolver null con un 200 engañoso.

Las solicitudes CRUD restantes siguen el mismo patrón:

curl http://localhost:3333/api/v1/posts                    # index
curl http://localhost:3333/api/v1/posts/1                  # show
curl -X PUT http://localhost:3333/api/v1/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated title"}'                         # update
curl -X PATCH http://localhost:3333/api/v1/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"body": "Updated body"}'                            # partial update
curl -X DELETE http://localhost:3333/api/v1/posts/1         # destroy → 204

Próximos Pasos

Una API CRUD de AdonisJS v7 necesita tres componentes adicionales antes de estar lista para producción: autenticación, paginación y pruebas.

En cuanto a la autenticación, el starter de API ya incluye rutas de autenticación y una configuración orientada a tokens de acceso. Protege las rutas de posts con el middleware de autenticación cuando deban estar disponibles únicamente para usuarios autenticados. Si estás agregando autenticación a un proyecto existente que no usa el starter, el comando documentado es:

node ace add @adonisjs/auth --guard=access_tokens

Para colecciones grandes, reemplaza Post.all() en index con paginación de Lucid para que el endpoint devuelva una página a la vez en lugar de toda la tabla.

Para las pruebas, utiliza Japa, el ejecutor de pruebas configurado por el starter. El starter de API v7 usa:

npm run test

Comienza probando los tres contratos que más importan al frontend: una creación exitosa devuelve 201, los fallos de validación devuelven 422 con errores a nivel de campo, y los registros inexistentes devuelven 404.

Preguntas Frecuentes

¿Por qué este tutorial de v7 usa rutas explícitas en lugar de router.resource().apiOnly()?

La documentación de enrutamiento actual de AdonisJS v7 documenta prominentemente router.resource(), pero una ruta de recurso simple registra siete rutas, incluyendo GET /posts/create y GET /posts/:id/edit para formularios HTML. Para una API exclusivamente JSON, las rutas explícitas GET, POST, PUT, PATCH y DELETE hacen que la superficie pública de la API sea evidente y evitan registrar accidentalmente rutas para mostrar formularios.

¿Por qué node ace make:controller --resource genera siete métodos cuando esta API solo usa cinco?

El flag --resource genera el conjunto completo de métodos del controlador RESTful: index, create, store, show, edit, update y destroy. Los métodos create y edit existen para renderizar formularios HTML en aplicaciones con renderizado en el servidor. Una API JSON solo necesita index, store, show, update y destroy, por lo que los métodos create y edit no utilizados pueden eliminarse sin problema.

¿Qué métodos HTTP debe soportar el endpoint de actualización?

Este tutorial mapea tanto PUT como PATCH en /api/v1/posts/:id al mismo método del controlador update. PUT generalmente indica un reemplazo completo del recurso y PATCH una actualización parcial, pero muchas APIs CRUD manejan ambos a través de la misma lógica de validación y fusión. Ejecuta node ace list:routes para confirmar el mapeo activo en tu aplicación.

¿Funcionará un tutorial de AdonisJS v6 en v7?

No de manera confiable. Gran parte de la lógica del controlador sigue siendo familiar, pero v7 cambia detalles importantes de los tutoriales: el requisito de entorno de ejecución es Node.js 24+, el andamiaje usa npm create adonisjs@latest, los starter kits incluyen Lucid y autenticación de forma predeterminada, los ejemplos de enrutamiento actuales usan #generated/controllers, y los modelos Lucid extienden clases de esquema generadas desde #database/schema. Trata los tutoriales de v6 únicamente como referencias conceptuales.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.