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.
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 v6npm 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
errorsque 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
Discover how at OpenReplay.com.
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étodo | Propósito |
|---|---|
index | Listar todos los posts |
store | Crear un nuevo post a partir del cuerpo de la solicitud |
show | Devolver un post individual por id |
update | Modificar un post existente por id |
destroy | Eliminar 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étodo | Ruta | Método del controlador | Descripción |
|---|---|---|---|
| GET | /api/v1/posts | index | Listar todos los posts |
| POST | /api/v1/posts | store | Crear un post |
| GET | /api/v1/posts/:id | show | Obtener un post |
| PUT | /api/v1/posts/:id | update | Actualizar un post |
| PATCH | /api/v1/posts/:id | update | Actualizar parcialmente un post |
| DELETE | /api/v1/posts/:id | destroy | Eliminar 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:
storedevuelveresponse.created(post), lo que establece HTTP 201.destroydevuelveresponse.noContent(), lo que establece HTTP 204.await post.delete()se espera conawait, 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.
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