AdonisJSでCRUD APIを構築する方法
AdonisJS v7でCRUD APIを構築。postsルート、Lucidモデル、VineJS検証、curlで動作確認するJSONレスポンスまで解説。
AdonisJSは、TypeScriptファーストのLaravel風Node.jsフレームワークであり、ルーティング、ORM(Lucid)、バリデーション(VineJS)、認証、CLI(ace)をひとつのパッケージとして提供します。これにより、規約に従ってCRUD APIを構築したい場合に非常に適した選択肢となります。本チュートリアルでは、AdonisJS v7を使用して、postsリソースの完全なCRUDエンドポイントを構築します。スキャフォールドからcurlによるテストまでのすべての手順を網羅し、VineJSによるバリデーションと構造化されたエラーレスポンスについても解説します。
AdonisJS v7はv6から3つの点で異なり、本チュートリアルはその変更点を反映しています。セットアップ面では(Node.js 24以上、npm create adonisjs@latestを使用)、APIスターターキット面では(バックエンドがapps/backend/配下に配置されたTurborepoモノレポ構成で、Lucid ORMと認証が事前設定済み)、そしてコードパターン面では(#generated/controllersを通じたルーティング、apps/backend/database/schema.tsから生成されたスキーマクラスを継承するLucidモデル)という変更があります。以下の例はすべてv7の構造に従っています。
重要なポイント
- 現在のAdonisJS v7 APIスターターは、旧v6の
npm init adonisjs@latest ... -- -K=api形式ではなく、npm create adonisjs@latest my-api -- --kit=apiでスキャフォールドします。 - AdonisJS v7はNode.js 24以上およびnpm 11以上を必要とするため、プロジェクトをスキャフォールドする前にローカルのランタイムを更新してください。
- 新しいv7スターターキットには、Lucid ORM、SQLite、認証、VineJS、テスト環境がすぐに使える状態で含まれているため、新規APIスターターにLucidを手動で追加する必要はありません。
- 現在のv7ルーティングの例では、生成されたコントローラーバレルインポート
import { controllers } from '#generated/controllers'を使用し、[controllers.Posts, 'index']形式のハンドラーでルートをバインドします。 - APIリクエストでVineJSのバリデーションが失敗した場合、AdonisJSはHTTP 422を返し、フィールド名・失敗したルール・メッセージを含む構造化された
errors配列を返します。これはフロントエンドのフォームライブラリがフィールドレベルのエラー表示に期待する形式です。 - Lucidの
Post.findOrFail(id)は、レコードが見つからない場合に例外をスローし、AdonisJSはそれを自動的に404 JSONレスポンスとして返します。手動でnullチェックを行う必要はありません。
前提条件
AdonisJS v7はNode.js 24以上およびnpm 11以上を必要とします。スキャフォールドを実行する前に、ローカル環境のバージョンを確認してください。
node --version # >= 24.x であること
npm --version # >= 11.x であること
v7スターターキットにはLucidがSQLiteをデフォルトで設定した状態で含まれているため、このチュートリアルを完了するためにPostgreSQLから始める必要はありません。SQLiteはローカルファイルに書き込むため、使い捨てのCRUDチュートリアルに最適です。後でPostgreSQL、MySQL、または他のSQLデータベースに移行する場合も、Lucidはそれらのドライバーをサポートしており、以下のコントローラーとバリデーションのコードはそのまま使用できます。
APIスターターキットでプロジェクトをスキャフォールドする
AdonisJS v7のAPIスターターはnpm create adonisjs@latest <name> -- --kit=apiでスキャフォールドします。今すぐ実行してください。
npm create adonisjs@latest my-api -- --kit=api
--kit=apiフラグはAPIスターターキットを選択します。このキットは、Lucid ORM、VineJSバリデーション、認証、CORS、テスト、デフォルトのSQLiteデータベースを含む、APIに特化したプロジェクトを提供します。
プロジェクトディレクトリに移動して開発サーバーを起動します。
cd my-api
npm run dev
バックエンドはデフォルトでhttp://localhost:3333で起動します。APIスターターには/api/v1/auth配下に認証エンドポイントも含まれていますが、本チュートリアルではCRUDに集中するため、postsエンドポイントは認証なしで公開します。
デフォルトのLucidセットアップを使用する
Discover how at OpenReplay.com.
LucidはAdonisJSのSQL ORMです。古いチュートリアルでは、次のような手動セットアップ手順が記載されていることがあります。
node ace add @adonisjs/lucid
新規v7 APIスターターではこのコマンドを実行しないでください。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 {}
これがv7のLucidモデルパターンです。カラムデコレーターは生成されたスキーマクラスに定義され、アプリのモデルはリレーションシップ、フック、クエリスコープ、カスタムメソッドを後から追加する場所となります。
コントローラーを生成する
node ace make:controller posts --resourceは、postsリソース用のリソースフルコントローラーをスキャフォールドします。今すぐ実行してください。
node ace make:controller posts --resource
--resourceフラグは、index、create、store、show、edit、update、destroyという標準的なRESTメソッドをスキャフォールドします。JSON APIに必要なのはそのうち5つです。
| メソッド | 用途 |
|---|---|
index | 全投稿の一覧取得 |
store | リクエストボディから新規投稿を作成 |
show | IDで単一の投稿を返す |
update | IDで既存の投稿を更新 |
destroy | IDで投稿を削除 |
createとeditメソッドはサーバーサイドレンダリングのHTMLフォーム用に存在するため、削除して構いません。
ルートを設定する
現在のv7ドキュメントでは、コントローラーを生成されたバレルファイル#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')
これにより、v7 APIスターターで使用されるバージョン管理されたAPIスタイルに合わせて、/api/v1/posts配下にAPIエンドポイントが登録されます。
| メソッド | パス | コントローラーメソッド | 説明 |
|---|---|---|---|
| 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は引き続きRESTfulルート用にrouter.resource()を文書化していますが、通常のリソースルートはHTMLフォーム用のGET /posts/createやGET /posts/:id/editを含む7つのルートを登録します。上記の明示的なルートリストは、APIのインターフェースをJSON専用に保ち、フォーム表示用のルートが誤って公開されることを防ぎます。
次のコマンドで有効なルートを確認できます。
node ace list:routes
ルートが表示されない場合は、生成されたコントローラーバレルファイルを更新するために開発サーバーを再起動してください。
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()がリクエストボディに対してバリデーターを実行し、型付きのバリデーション済みペイロードを返します。
APIリクエストでrequest.validateUsing()が失敗した場合、AdonisJSはHTTP 422と構造化されたJSON errors配列を返します。各エラーにはフィールド名、失敗したルール、メッセージが含まれます。これはフロントエンドのコードがフォームフィールドの状態に直接マッピングできる形式です。
CRUD APIを使用するフロントエンドのセッションリプレイでは、特定の障害パターンが頻繁に見られます。フォームが送信され、サーバーが422を返しても、UIにフィールドレベルのエラーメッセージが一切表示されないというものです。根本的な原因は、サーバーが構造化されたフィールドエラーではなく自由形式の文字列を返したことにあります。VineJSはデフォルトで構造化された形式を提供するため、それを維持してください。フィールドレベルのコントラクトを維持しないまま、バリデーション例外をキャッチしてフラットな文字列として再スローしないようにしてください。
コントローラーメソッドを実装する
各コントローラーアクションは簡潔なハンドラーです。重要な2つの失敗パス(レコードが見つからない場合と無効な入力)は、手動の分岐処理なしに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でテストする
AdonisJS v7のCRUD APIはcurlでエンドツーエンドのテストが可能です。すべてのリソースアクション(index、store、show、update、destroy)は/api/v1/posts配下の単一のHTTPリクエストにマッピングされます。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は誤解を招く200を返す代わりに404レスポンスを生成します。
残りの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
次のステップ
AdonisJS v7のCRUD APIを本番環境に対応させるには、認証、ページネーション、テストという3つのフォローアップが必要です。
認証については、APIスターターにすでに認証ルートとアクセストークン指向のセットアップが含まれています。ログインユーザーのみがアクセスできるようにする場合は、postsルートを認証ミドルウェアで保護してください。スターターキット以外の既存プロジェクトに認証を追加する場合、ドキュメントに記載されているコマンドは次のとおりです。
node ace add @adonisjs/auth --guard=access_tokens
大量のデータを扱う場合は、indexのPost.all()をLucidのページネーションに置き換えて、テーブル全体ではなく1ページずつ返すようにしてください。
テストについては、スターターキットで設定されているテストランナーのJapaを使用してください。v7 APIスターターでは次のコマンドを使用します。
npm run test
フロントエンドにとって最も重要な3つのコントラクトのテストから始めてください。作成が成功した場合は201を返すこと、バリデーションが失敗した場合はフィールドレベルのエラーとともに422を返すこと、そして存在しないレコードの場合は404を返すことです。
よくある質問
このv7チュートリアルでrouter.resource().apiOnly()の代わりに明示的なルートを使用しているのはなぜですか?
現在のAdonisJS v7のルーティングドキュメントではrouter.resource()が prominentに記載されていますが、通常のリソースルートはHTMLフォーム用のGET /posts/createとGET /posts/:id/editを含む7つのルートを登録します。JSON専用のAPIでは、明示的なGET、POST、PUT、PATCH、DELETEルートを使用することで、公開APIのインターフェースが明確になり、フォーム表示用のルートが誤って登録されることを防ぐことができます。
node ace make:controller --resourceが7つのメソッドをスキャフォールドするのに、このAPIで使用するのは5つだけなのはなぜですか?
--resourceフラグはRESTfulコントローラーの完全なセット(index、create、store、show、edit、update、destroy)をスキャフォールドします。createとeditメソッドは、サーバーサイドレンダリングアプリでHTMLフォームを表示するために存在します。JSON APIに必要なのはindex、store、show、update、destroyのみであるため、未使用のcreateとeditメソッドは安全に削除できます。
updateエンドポイントはどのHTTPメソッドをサポートすべきですか?
このチュートリアルでは、/api/v1/posts/:idに対するPUTとPATCHの両方を同じ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