Как создать CRUD API с AdonisJS
Создайте CRUD API на AdonisJS v7: маршруты posts, модели Lucid, валидация VineJS и JSON-ответы, проверенные через curl.
AdonisJS — это Node.js-фреймворк с первоклассной поддержкой TypeScript, вдохновлённый Laravel. В одной поставке он включает маршрутизацию, ORM (Lucid), валидацию (VineJS), аутентификацию и CLI (ace) — что делает его отличным выбором для CRUD API, когда вы предпочитаете готовые соглашения ручной настройке. В этом руководстве мы создадим полноценный CRUD-эндпоинт posts с использованием AdonisJS v7, охватив весь путь от генерации проекта до протестированного curl-запроса, включая валидацию через VineJS и структурированные ответы об ошибках.
AdonisJS v7 отличается от v6 в трёх аспектах, которые отражены в этом руководстве: настройка (Node.js 24+, npm create adonisjs@latest); API-стартер (монорепозиторий на базе Turborepo с бэкендом в директории apps/backend/, с предустановленными Lucid ORM и аутентификацией); и паттерны кода (маршрутизация через #generated/controllers, модели Lucid, расширяющие сгенерированные классы схем из apps/backend/database/schema.ts). Все примеры ниже следуют структуре v7.
Ключевые выводы
- Текущий API-стартер AdonisJS v7 создаётся командой
npm create adonisjs@latest my-api -- --kit=api, а не устаревшей формой v6npm init adonisjs@latest ... -- -K=api. - AdonisJS v7 требует Node.js 24+ и npm 11+, поэтому обновите локальную среду выполнения перед созданием проекта.
- Новые стартовые наборы v7 включают Lucid ORM, SQLite, аутентификацию, VineJS и настройку тестирования «из коробки» — добавлять Lucid вручную в свежий API-стартер не нужно.
- В текущих примерах маршрутизации v7 контроллеры импортируются через сгенерированный barrel-файл:
import { controllers } from '#generated/controllers', а маршруты привязываются обработчиками вида[controllers.Posts, 'index']. - Ошибки валидации VineJS при API-запросах возвращают HTTP 422 со структурированным массивом
errors, содержащим имена полей, нарушенные правила и сообщения — именно тот формат, который ожидают фронтенд-библиотеки форм для отображения ошибок на уровне полей. - Метод
Post.findOrFail(id)в Lucid выбрасывает исключение «запись не найдена», которое AdonisJS автоматически преобразует в JSON-ответ с кодом 404 — без необходимости вручную проверять наnull.
Предварительные требования
AdonisJS v7 требует Node.js версии 24 или выше и npm версии 11 или выше. Проверьте локальные версии перед созданием проекта:
node --version # must be >= 24.x
npm --version # must be >= 11.x
Стартовые наборы v7 включают Lucid, настроенный с SQLite по умолчанию, поэтому для прохождения этого руководства не нужно сразу переходить на PostgreSQL. SQLite записывает данные в локальный файл и отлично подходит для учебного CRUD-проекта. Если впоследствии вы перейдёте на PostgreSQL, MySQL или другую SQL-базу данных, Lucid поддерживает эти драйверы; код контроллеров и валидации, приведённый ниже, останется без изменений.
Создание проекта с API-стартером
API-стартер AdonisJS v7 создаётся командой npm create adonisjs@latest <name> -- --kit=api. Выполните её сейчас:
npm create adonisjs@latest my-api -- --kit=api
Флаг --kit=api выбирает API-стартер. Он создаёт проект, ориентированный на бэкенд, с настройками по умолчанию для API: Lucid ORM, валидация VineJS, аутентификация, CORS, тестирование и база данных SQLite.
Перейдите в директорию проекта и запустите сервер разработки:
cd my-api
npm run dev
По умолчанию бэкенд запускается на http://localhost:3333. API-стартер также включает эндпоинты аутентификации по пути /api/v1/auth, однако в этом руководстве эндпоинты posts остаются публичными, чтобы сосредоточиться на CRUD.
Использование встроенной настройки Lucid
Discover how at OpenReplay.com.
Lucid — это SQL ORM для AdonisJS. В устаревших руководствах вы часто встретите ручной шаг установки:
node ace add @adonisjs/lucid
Не выполняйте эту команду в свежем API-стартере v7. Lucid уже установлен и настроен, а стартер использует SQLite по умолчанию. Можно сразу переходить к созданию таблицы.
Генерация миграции
Миграция — это версионированное изменение схемы базы данных. Создайте миграцию для таблицы posts:
node ace make:migration posts
Команда создаёт файл с временной меткой в директории database/migrations/. Откройте его и определите столбцы таблицы:
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)
}
}
Путь импорта по-прежнему @adonisjs/lucid/schema. Важное нововведение v7 появляется после выполнения миграции: Lucid генерирует классы схем на основе вашей базы данных, чтобы модель приложения могла наследовать типизированные определения столбцов.
Выполнение миграции и создание модели
Примените схему к базе данных:
node ace migration:run
Lucid создаёт таблицу posts и генерирует файл database/schema.ts. Этот сгенерированный файл содержит класс PostsSchema с типизированными определениями столбцов таблицы. Не редактируйте database/schema.ts напрямую — он перегенерируется после каждой миграции.
Теперь создайте модель:
node ace make:model Post
Откройте app/models/post.ts и сделайте так, чтобы модель расширяла сгенерированный класс схемы, экспортированный из database/schema.ts:
import { PostSchema } from '#database/schema'
export default class Post extends PostSchema {}
Это паттерн модели Lucid в v7. Декораторы столбцов находятся в сгенерированном классе схемы, тогда как модель приложения — это место, где впоследствии добавляются связи, хуки, скоупы запросов и пользовательские методы.
Генерация контроллера
Команда node ace make:controller posts --resource создаёт resourceful-контроллер для ресурса posts. Выполните её сейчас:
node ace make:controller posts --resource
Флаг --resource генерирует стандартные REST-методы: index, create, store, show, edit, update и destroy. Для JSON API нужны только пять из них:
| Метод | Назначение |
|---|---|
index | Получить список всех записей |
store | Создать новую запись из тела запроса |
show | Вернуть одну запись по id |
update | Изменить существующую запись по id |
destroy | Удалить запись по id |
Сгенерированные методы create и edit можно удалить — они предназначены для серверно-рендеримых HTML-форм.
Настройка маршрутов
В текущей документации v7 контроллеры импортируются через сгенерированный barrel-файл #generated/controllers. Откройте start/routes.ts и добавьте маршруты для 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')
Это регистрирует API-эндпоинты по пути /api/v1/posts, соответствуя версионированному стилю API, используемому в API-стартере v7.
| Метод | Путь | Метод контроллера | Описание |
|---|---|---|---|
| GET | /api/v1/posts | index | Список всех записей |
| POST | /api/v1/posts | store | Создать запись |
| GET | /api/v1/posts/:id | show | Получить одну запись |
| PUT | /api/v1/posts/:id | update | Обновить запись |
| PATCH | /api/v1/posts/:id | update | Частично обновить запись |
| DELETE | /api/v1/posts/:id | destroy | Удалить запись |
AdonisJS по-прежнему документирует router.resource() для RESTful-маршрутов, однако обычный resource-маршрут регистрирует семь маршрутов, включая GET /posts/create и GET /posts/:id/edit. Явный список маршрутов выше делает поверхность API исключительно JSON-ориентированной и исключает случайную регистрацию маршрутов для отображения форм.
Проверьте активные маршруты командой:
node ace list:routes
Если маршруты не отображаются, перезапустите сервер разработки, чтобы обновить сгенерированный barrel-файл контроллеров.
Добавление валидации VineJS
VineJS — библиотека валидации, используемая в AdonisJS. Создайте файл валидатора:
node ace make:validator post
Откройте app/validators/post.ts и определите валидаторы для создания и обновления записей:
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(),
})
В текущем руководстве по валидации AdonisJS для определения валидаторов используется vine.create(). В контроллерах метод request.validateUsing() запускает валидатор против тела запроса и возвращает типизированные, прошедшие валидацию данные.
Если request.validateUsing() завершается с ошибкой для API-запроса, AdonisJS возвращает HTTP 422 со структурированным JSON-массивом errors. Каждая ошибка содержит имя поля, нарушенное правило и сообщение. Именно этот формат фронтенд-код может напрямую отобразить в состояние полей формы.
Сессионные записи фронтендов, потребляющих CRUD API, часто выявляют одну характерную проблему: форма отправляется, сервер возвращает 422, но сообщение об ошибке на уровне поля так и не появляется в интерфейсе. Причина, как правило, в том, что сервер вернул произвольную строку вместо структурированных ошибок по полям. VineJS по умолчанию предоставляет структурированный формат — сохраняйте его. Не перехватывайте исключение валидации и не перебрасывайте его в виде плоской строки, если только вы не сохраняете при этом контракт на уровне полей.
Реализация методов контроллера
Каждое действие контроллера — это компактный обработчик. Два сценария ошибок, которые имеют значение — отсутствующая запись и некорректные входные данные — обрабатываются Lucid и VineJS без ручного ветвления.
Откройте app/controllers/posts_controller.ts и замените его содержимое:
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()
}
}
Несколько важных деталей:
storeвозвращаетresponse.created(post), устанавливая HTTP 201.destroyвозвращаетresponse.noContent(), устанавливая HTTP 204.await post.delete()выполняется сawait, поэтому ответ не возвращается до завершения удаления.Post.findOrFail(params.id)выбрасывает исключение, если строка не найдена. AdonisJS автоматически преобразует это исключение в ответ с кодом 404.
Тестирование с помощью curl
CRUD API на AdonisJS v7 можно проверить сквозным образом с помощью curl — каждое действие ресурса (index, store, show, update, destroy) соответствует одному HTTP-запросу по пути /api/v1/posts. При запущенном npm run dev создайте запись:
curl -X POST http://localhost:3333/api/v1/posts \
-H "Content-Type: application/json" \
-d '{"title": "First post", "body": "Hello from Adonis"}'
Успешный ответ store возвращает HTTP 201 с созданной записью:
{
"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
}
Теперь спровоцируйте ошибки валидации, отправив слишком короткий заголовок и пустое тело:
curl -X POST http://localhost:3333/api/v1/posts \
-H "Content-Type: application/json" \
-d '{"title": "ok", "body": ""}'
Ответ — HTTP 422 со структурированным массивом ошибок 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"
}
]
}
Это контракт, который фронтенд отображает в состояние формы: перебираем errors, индексируем по field и выводим message рядом с соответствующим полем ввода.
Запросите несуществующую запись, чтобы увидеть сценарий с кодом 404:
curl -i http://localhost:3333/api/v1/posts/9999
findOrFail возвращает ответ 404 вместо того, чтобы вернуть null с вводящим в заблуждение кодом 200.
Остальные CRUD-запросы следуют той же схеме:
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
Дальнейшие шаги
Перед выходом в production CRUD API на AdonisJS v7 требует трёх доработок: аутентификации, пагинации и тестов.
Для аутентификации API-стартер уже включает маршруты auth и настройку на основе токенов доступа. Защитите маршруты posts с помощью middleware аутентификации, если они должны быть доступны только авторизованным пользователям. Если вы добавляете аутентификацию в существующий проект, не созданный на основе стартера, используйте документированную команду:
node ace add @adonisjs/auth --guard=access_tokens
Для больших коллекций замените Post.all() в методе index на пагинацию Lucid, чтобы эндпоинт возвращал данные постранично, а не всю таблицу целиком.
Для тестирования используйте Japa — тест-раннер, настроенный в стартере. В API-стартере v7 тесты запускаются командой:
npm run test
Начните с проверки трёх контрактов, наиболее важных для фронтенда: успешное создание возвращает 201, ошибки валидации возвращают 422 с ошибками на уровне полей, а отсутствующие записи возвращают 404.
Часто задаваемые вопросы
Почему в этом руководстве по v7 используются явные маршруты вместо router.resource().apiOnly()?
Текущая документация по маршрутизации AdonisJS v7 активно документирует router.resource(), однако обычный resource-маршрут регистрирует семь маршрутов, включая GET /posts/create и GET /posts/:id/edit для HTML-форм. Для JSON-only API явные маршруты GET, POST, PUT, PATCH и DELETE делают поверхность публичного API очевидной и исключают случайную регистрацию маршрутов для отображения форм.
Почему node ace make:controller --resource генерирует семь методов, если API использует только пять?
Флаг --resource создаёт полный набор RESTful-методов контроллера: index, create, store, show, edit, update и destroy. Методы create и edit предназначены для рендеринга HTML-форм в серверно-рендеримых приложениях. JSON API требует только index, store, show, update и destroy, поэтому неиспользуемые методы create и edit можно безопасно удалить.
Какие HTTP-методы должен поддерживать эндпоинт обновления?
В этом руководстве и PUT, и PATCH по пути /api/v1/posts/:id направляются к одному и тому же методу контроллера update. PUT обычно означает полную замену ресурса, а PATCH — частичное обновление, однако многие CRUD API обрабатывают оба варианта через одну и ту же логику валидации и слияния. Выполните node ace list:routes, чтобы проверить актуальное сопоставление в вашем приложении.
Будет ли руководство по AdonisJS v6 работать на v7?
Ненадёжно. Большая часть логики контроллеров выглядит знакомо, однако v7 меняет важные детали: требуется Node.js 24+, создание проекта выполняется через npm create adonisjs@latest, стартовые наборы включают Lucid и аутентификацию по умолчанию, текущие примеры маршрутизации используют #generated/controllers, а модели Lucid расширяют сгенерированные классы схем из #database/schema. Рассматривайте руководства по v6 исключительно как концептуальные справочники.
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