Rails/LaravelエンジニアのためのフルスタックTypeScriptフレームワーク「AdonisJS」
はじめに
この記事では、RailsやLaravelに精通したエンジニアに向けて、フルスタックTypeScriptフレームワーク「AdonisJS」を紹介します。
AdonisJSは、これまでRuby on RailsやLaravelでの開発に慣れ親しんだエンジニアにとって、とても親しみやすいフレームワークで、どれほど似ているか比較してみようと思います。
AdonisJSとは?
AdonisJSとはTypeScriptベースのフルスタックWebフレームワークです。
MVCパターンを採用しており、Ruby on RailsやLaravelのようにルーティング、コントローラ、ORMなど公式パッケージが提供されていてエコシステムが整っているのが特徴です。
また、型安全性を重視しているのも良い点です。
プロジェクト作成
AdonisJS
npm init adonisjs@latest blog
AdonisJSの場合は基本的にスターターキットを選択してプロジェクトを作成します。
テンプレートエンジン(EdgeJS)を使った昔ながらのWeb starter kitや、SPA(InertiaJS)のInertia starter kit、RailsのAPIモードのようなAPIサーバー構築用のAPI starter kitなどが選択肢にあります。
詳しくはドキュメントを参照ください。
Ruby on Rails
rails new blog
Railsの場合は公式で提供しているスターターキット的なものはない認識で、-m
オプションを使うと自分または誰かの用意したテンプレートを適用することができます。
rails new blog -m ~/template.rb
Laravel
composer create-project laravel/laravel blog
Docker環境で開発したい場合はSailというツールも用意されています。
Laravel公式のスターターキットにはBreezeとJetstreamというものがあり、Breezeは基本的な機能が提供されており、Jetstreamの方はより高機能というイメージです。
プロジェクト作成時に利用するか聞かれるはずですし、後からインストールすることもできます。
こちらもテンプレートエンジン(Blade)、SPA(InertiaJS)が選べますし、PHPでダイナミックなUIを表現できるLivewireを選ぶことができます。
Routing
AdonisJS
AdonisJSのルーティングはstart/routes.tsに記述します。
import router from '@adonisjs/core/services/router'
router.get('/', () => {
return 'Hello world from the home page.'
})
router.get('/about', () => {
return 'This is the about page.'
})
router.get('/posts/:id', ({ params }) => {
return `This is post with id ${params.id}`
})
上記例はインラインですが、もちろんコントローラを設定することもできます。
import UsersController from '#controllers/users_controller'
router.post('users', [UsersController, 'store'])
さらにresource
メソッドでCRUD処理を省略して定義することもできます。
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.resource('posts', PostsController)
これで以下のようなルートが定義されます。
定義したルートの一覧を確認するコマンドもあります。
node ace list:routes
また、AdonisJS公式のVSCode拡張もあり、そこでルート一覧を確認することもできます。
Ruby on Rails
Railsの場合はconfig/routes.rbに記述します。
Rails.application.routes.draw do
# インライン
get '/health', to: ->(env) { [204, {}, ['']] }
# コントローラあり
get '/patients/:id', to: 'patients#show'
# リソース
resources :photos
end
さすがRuby on Railsですね。めちゃめちゃシンプルに書けます。
ルート一覧を出力するコマンドは以下です。
rails routes
Laravel
<?php
use App\Http\Controllers\UserController;
use App\Http\Controllers\PhotoController;
// インライン
Route::get('/greeting', function () {
return 'Hello World';
});
// コントローラ
Route::get('/user', [UserController::class, 'index']);
Route::resource('photos', PhotoController::class);
AdonisJSとかなり似ています。
Laravelエンジニアの方は違和感なくAdonisJSのルーティングを書くことができそうです。
ルート一覧コマンドは以下のようになります。
php artisan route:list
また、モデルをタイプヒントで値を受け取れるようにもできるようです。
use App\Http\Controllers\UserController;
use App\Models\User;
// Route definition...
Route::get('/users/{user}', [UserController::class, 'show']);
// Controller method definition...
public function show(User $user)
{
return view('user.profile', ['user' => $user]);
}
他にもさまざまな書き方ができるようなので、詳しくはドキュメントを参照してください。
Controller
AdonisJS
node ace make:controller posts --resource
--resource
オプションを付けることで、CRUDに必要なメソッドをscaffoldしてくれます。
import type { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
/**
* Return list of all posts or paginate through
* them
*/
async index({}: HttpContext) {}
/**
* Render the form to create a new post.
*
* Not needed if you are creating an API server.
*/
async create({}: HttpContext) {}
/**
* Handle form submission to create a new post
*/
async store({ request }: HttpContext) {}
/**
* Display a single post by id.
*/
async show({ params }: HttpContext) {}
/**
* Render the form to edit an existing post by its id.
*
* Not needed if you are creating an API server.
*/
async edit({ params }: HttpContext) {}
/**
* Handle the form submission to update a specific post by id
*/
async update({ params, request }: HttpContext) {}
/**
* Handle the form submission to delete a specific post by id.
*/
async destroy({ params }: HttpContext) {}
}
AdonisJSではDependency Injection(DI)にも対応しています。
まず、ビジネスロジックを担うサービスクラスを用意します。
export default class UserService {
async all() {
// return users from db
}
}
これをコントローラにDIできます。
import { inject } from '@adonisjs/core'
import UserService from '#services/user_service'
@inject()
export default class UsersController {
constructor(protected userService: UserService) {}
index() {
return this.userService.all()
}
}
Ruby on Rails
rails generate controller posts
AdonisJSの--resource
のようなオプションはないようですが、rails generate scaffold posts
というモデルやビューなどを一気に生成してくれるコマンドを実行した場合は以下のようなコントローラファイルとアクションが生成されます(わかりやすいようアクション内の処理は消しています)。
class PostsController < ApplicationController
def index
end
def show
end
def new
end
def edit
end
def create
end
def update
end
def destroy
end
end
Railsの場合は引数は特になく、request
オブジェクトとresponse
オブジェクトがそのまま使えます。
ビューのレンダリングも何も書かなくても上記の場合はapp/views/posts/index.html.erbのようなファイルがあればレンダリングされます。
かなり省略して書けるイメージです。
Laravel
php artisan make:controller PostsController --resource
Laravelは--resource
オプションがあり、以下のようにファイルが作られます。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostsController extends Controller
{
public function index()
{
}
public function create()
{
}
public function store(Request $request)
{
}
public function show($id)
{
}
public function edit($id)
{
}
public function update(Request $request, $id)
{
}
public function destroy($id)
{
}
}
アクション名がAdonisJSと同じですね。Railsエンジニアの方はcreateが新規登録の画面描画用であることに注意が必要です。
また、LaravelでもDependency Injectionが可能です。
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
}
Migration
AdonisJS
node ace make:migration users
作成されるマイグレーションファイルは以下のようになります。
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
テーブルの変更は--alter
オプションをつけて実行します。
node ace make:migration posts --alter
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'posts'
async up() {
this.schema.alterTable(this.tableName, (table) => {
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
})
}
}
マイグレーション実行コマンドは以下のようになります。
node ace migration:run
Ruby on Rails
Railsはプレフィックスでテーブルの作成、変更を判断してファイルが生成されます。
rails generate migration create_users email:string
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :email
t.timestamps
end
end
end
rails generate migration add_title_body_published_to_post title:string body:text published:boolean
class AddTitleBodyPublishedToPost < ActiveRecord::Migration[7.1]
def change
add_column :posts, :title, :string
add_column :posts, :body, :text
add_column :posts, :published, :boolean
end
end
マイグレーション実行コマンドは以下のようになります。
rails db:migrate
Laravel
まずテーブル作成です。
php artisan make:migration create_users_table --create=users
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
次にテーブルの更新です。
php artisan make:migration modify_users_table --table=users
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
};
マイグレーションの実行コマンドは以下です。
php artisan migrate
Model
AdonisJS
AdonisJSはLucid ORMというKnexベースで作られたActive RecordパターンのORMが提供されています。
モデルの作成コマンドは以下のようになります。
node ace make:model User
Railsのようにマイグレーションファイルも一緒に作りたい場合は、-m
オプションを付けることができます。
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'
export default class User extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
CRUD処理は以下のように書けます。
import User from '#models/user'
/**
* Create a new user
*/
const user = await User.create({
username: 'rlanz',
email: 'romain@adonisjs.com',
})
/**
* Find a user by primary key
*/
const user = await User.find(1)
/**
* Update a user
*/
const user = await User.find(1)
user.username = 'romain'
await user.save()
/**
* Delete a user
*/
const user = await User.find(1)
await user.delete()
詳しくはドキュメントに書かれています。
Ruby on Rails
RailsのORMはみなさんご存知のActive Recordです。
rails generate model post title:string body:text published:boolean
Railsはマイグレーションファイルとセットで生成されるので、カラムを指定することが可能です。
class Post < ApplicationRecord
end
モデルファイルはAdonisJSと違いクラスの中はカラムを定義しなくても良いです。
使い方の例は以下のようになります。
# Create a new book
book = Book.create(title: "The Lord of the Rings", author: "J.R.R. Tolkien")
# Find a book
book = Book.find(1)
# Update a book
book = Book.find(1)
book.update(title: "The Lord of the Rings: The Fellowship of the Ring")
# Delete a book
book = Book.find(1)
book.destroy
更新処理はAdonisJSと同じくsave
メソッドを使ったやり方もできますが、update
メソッドもあるのでそちらの方がシンプルに書けますね。
Laravel
LaravelのORMはEloquentです。
php artisan make:model Post
こちらもマイグレーションファイルを一緒に作成したい場合は-m
オプションを付けます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
}
モデルファイルはRailsと同じで特にクラス内は何も書かなくても動きます。
次に使い方を見てみます。
use App\Models\Post;
// Create a new post
$post = Post::create([
'title' => 'My Post',
]);
// Find a post
$post = Post::find(1);
// Update a post
$post = Post::find(1);
$post->title = 'My Post';
$post->save();
// Delete a post
$post = Post::find(1);
$post->delete();
こちらもAdonisJSと基本同じように見えますが、AdonisJSだとcreate
メソッドのところなどではTypeScriptですのでカラム名の補完が効いて重宝しています。
View
AdonisJS
AdonisJSのビューではEdgeJSというテンプレートエンジンを使用します。
node ace make:view welcome
コントローラがレンダリングする際には以下のようになります。
export default class WelcomeController {
async index({ view }: HttpContext) {
return view.render('welcome', { username: 'romainlanz' })
}
}
ここでwelcome.edgeというファイルが存在すると補完が効きますし、コードジャンプもできて素晴らしいです。
.edgeファイルでは{{}}
でコントローラから渡した値を表示できます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>
Hello {{ username }}
</h1>
</body>
</html>
if文やfor文ももちろん使えます。
@if(user.hasSubscription)
Hurray! You have access to over 280 videos.
@else
Videos are available only to subscribers.
@end
@each(comment in post.comments)
@include('partials/comment')
@end
またEdgeJSとは別にInertiaJSによるSPAでビューを構築することもできます。
コントローラで以下のように書けます。
export default class HomeController {
async index({ inertia }: HttpContext) {
return inertia.render('home', { user: { name: 'julien' } })
}
}
InertiaJSではいくつものフロントエンドフレームワーク(Vue/React/Svelte/Solid)に対応しています。
例えばReactだと以下のように書けます。
export default function Home(props: { user: { name: string } }) {
return <p>Hello {props.user.name}</p>
}
このようにAPIのリクエスト処理を書かずに直接propsでコントローラから渡した値を受け取ることができます。
VueであればdefineProps
に渡ってきます。
<script setup lang="ts">
defineProps<{
user: { name: string }
}>()
</script>
<template>
<p>Hello {{ user.name }}</p>
</template>
ここで個人的に気に入っている部分がコントローラで返すpropsの型をフロントエンド側で共通して使うことができることです。
import { InferPageProps } from '@adonisjs/inertia/types'
import type HomeController from '#controllers/home_controller'
export function Home(
props: InferPageProps<HomeController, 'index'>
) {
return <p>Hello {props.user.name}</p>
}
こうすることでバックエンドからのデータ取得に関して齟齬が発生しにくくなります。
Ruby on Rails
RailsのテンプレートエンジンはデフォルトでERBで、特にビューを生成するコマンドは用意されていません(rails generate scaffold
コマンドからは生成できます)。
コントローラ側でも前述した通り、描画する処理は何も書かなくてもアクション名と同じビューファイルがあれば描画できます。
そしてコントローラ内でインスタンス変数を定義しておけば、それがビューでも使用できます。
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
この場合app/views/posts/index.html.erbファイルがあればそれが表示され、インスタンス変数も使用できます。
<h1>Posts</h1>
<% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
<% end %>
ここではapp/views/products/_post.html.erbというパーシャルというものを使って部分的にビューを切り出しています。
上記例はさらに省略して書くことができます。
<h1>Posts</h1>
<%= render partial: "post", collection: @posts %>
Post
インスタンスのコレクションという条件付きではありますが、これをさらに省略することができます。
<h1>Posts</h1>
<%= render @posts %>
さすがRuby on Rails、超絶シンプルになりました。
またRailsではTurboというJavaScriptを書かずにSPAのような振る舞いを実現できるライブラリが提供されています。
これを利用するとRubyまたはERBの記述のみでインタラクティブなアプリケーションを作れるようになります。
詳しくは公式サイトを参照ください。
Laravel
LaravelはテンプレートエンジンとしてBaldeを採用しています。
Laravelにはビューファイル生成コマンドが用意されています(昔はなかったような記憶ですが、最近できたようです)。
php artisan make:view welcome
コントローラ側からはview
というグローバルヘルパー関数を使用してビューを描画します。
<?php
namespace App\Http\Controllers;
class WelcomeController extends Controller
{
public function index(
) {
return view('welcome', ['name' => 'James']);
}
}
resources/views/welcome.blade.phpファイルを作成してビューが表示できます。
<html>
<body>
<h1>Hello, {{ $name }}</h1>
</body>
</html>
if文やfor文は以下のような書き方になります。
@if (count($records) === 1)
I have one record!
@elseif (count($records) > 1)
I have multiple records!
@else
I don't have any records!
@endif
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}
@endfor
@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach
また、LaravelにもRailsのTurboと同じようなアプローチでJavaScriptを書かずにインタラクティブなアプリケーションを実現できるLivewireというライブラリが提供されています。
逆にReactやVueなどをフロントエンドに採用したい場合はAdonisJSと同じInertiaJSに対応しています(元々InertiaJSはLaravel向けにチューニングされたもの)。
スターターキットのBreezeやJetstreamではビューのオプションとしてBlade/Livewire/InertiaJSの中から選択してプロジェクトを始めることができます。
Middleware
AdonisJS
ミドルウェアとはコントローラ処理の前後に処理を挟むことができるもので、例としてリクエストボディのパース、セッション管理、リクエストの認証、静的ファイルの配信などに使用できます。
AdonisJSでは3種類のミドルウェアがありそれぞれの用途は以下になります。
- Server middleware: ルーティングで定義されていないルート(静的ファイルや存在しないパスへのアクセスなど)も含め全てのHTTPリクエストで処理が走る。例えば静的ファイル配信に関しての処理など
- Router middleware: ルーティングで定義されたルートにマッチした全てのHTTPリクエスト。グローバルミドルウェア。例えばボディパーサー、セッションミドルウェアなど
- Named middleware: 各ルートまたはルートグループに明示的に適用しなければ処理が走らないミドルウェア。名前付きミドルウェア。例えばページはログインユーザーのみアクセスを許可したい時など
ミドルウェアの登録はstart/kernel.tsに記述していきます。
import server from '@adonisjs/core/services/server'
import router from '@adonisjs/core/services/router'
// Server middleware
server.use([
() => import('@adonisjs/static/static_middleware')
])
// Router middleware
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])
// Named middleware
router.named({
auth: () => import('#middleware/auth_middleware')
})
ミドルウェアファイルの作成にはコマンドが用意されています。
node ace make:middleware user_location
処理はhandle
メソッドの中に書くことになります。
ミドルウェアの処理が終わったらnext
関数を呼ぶことで、次のミドルウェア処理が走ります。
リクエストを弾きたい場合はException
を投げて終了させます。
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// throw new Exception('Aborting request')
await next()
}
}
作成した名前付きミドルウェアの各ルートへの適用は以下のように書けます。
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
const PostsController = () => import('#controllers/posts_controller')
router
.get('posts', [PostsController, 'index'])
.use(middleware.userLocation())
// 複数登録
router
.get('posts', [PostsController, 'index'])
.use([
middleware.userLocation(),
middleware.auth()
])
// グループ
router.group(() => {
router.get('posts', [PostsController, 'index'])
router.get('create', [PostsController, 'create'])
router.get('posts/:id', [PostsController, 'show'])
// ...
}).use(middleware.auth())
// リソース
router.resource('posts', PostsController).use('*', middleware.auth())
Ruby on Rails
Railsにもミドルウェアというものはありますが少し違っていて、AdonisJSのミドルウェアに近いものとしてはコントローラのアクションコールバック(before_action
やafter_action
など)になるかなと思います。
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
アクションコールバックはコントローラに処理を書くものになるので、ルートというよりはコントローラを継承して各アクションに適用していく形になります。
// 全アクションへrequire_loginを適用
class PostsController < ApplicationController
end
// ログインに関するアクションはスキップ
class LoginsController < ApplicationController
skip_before_action :require_login, only: [:new, :create]
end
このアプローチに関してはAdonisJSやLaravelと大きく異なる部分になります。
Laravel
LaravelのミドルウェアはAdonisJSと似たようなものになっています。
まず、ミドルウェア作成コマンドです。
php artisan make:middleware EnsureTokenIsValid
処理はAdonisJSと同じhandle
に記述、next
でリクエスト処理を続けます。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureTokenIsValid
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if ($request->input('token') !== 'my-secret-token') {
return redirect('/home');
}
return $next($request);
}
}
次にミドルウェアの登録についてです。
AdonisJSのServer middlweareに当たるものは見つけれなかったのですが、404などの処理に関してはRoute::fallback
メソッドにミドルウェアを適用さえる形で対応できるのかなと思いました。
静的ファイルへのミドルウェア適用はわからなかったので、わかる方がいたら教えていただきたいです。
グローバルミドルウェアはbootstrap/app.phpで設定します。
use App\Http\Middleware\EnsureTokenIsValid;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->append(EnsureTokenIsValid::class);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
一方ルート毎のミドルウェア適用はAdonisJSのように名前付きミドルウェアをわざわざ登録する必要はなく直接ルートに適用できます。
use App\Http\Middleware\EnsureTokenIsValid;
use App\Http\Middleware\First;
use App\Http\Middleware\Second;
Route::get('/profile', function () {
// ...
})->middleware(EnsureTokenIsValid::class);
// 複数のミドルウェア
Route::get('/', function () {
// ...
})->middleware([First::class, Second::class]);
// グループ
Route::middleware([EnsureTokenIsValid::class])->group(function () {
Route::get('/', function () {
// ...
});
// グループから除外
Route::get('/profile', function () {
// ...
})->withoutMiddleware([EnsureTokenIsValid::class]);
});
AdonisJSのように名前付きミドルウェアのようなことも可能で、ミドルウェアエイリアスやミドルウェアグループと呼ばれるようです。
bootstrap/app.phpで以下のように登録します。
->withMiddleware(function (Middleware $middleware) {
// ミドルウェアグループ
$middleware->appendToGroup('group-name', [
First::class,
Second::class,
]);
// ミドルウェアエイリアス
$middleware->alias([
'subscribed' => EnsureUserIsSubscribed::class
]);
})
ルーティングでの使い方は以下のようになります。
// ミドルウェアグループ
Route::get('/', function () {
// ...
})->middleware('group-name');
// ミドルウェアエイリアス
Route::get('/profile', function () {
// ...
})->middleware('subscribed');
Laravelにはまだまだ他にも色々な書き方があるようですので、詳しくはドキュメントを参照してください。
Validation
AdonisJS
最後はバリデーションを比べてみましょう。
AdonisJSではバリデーションライブラリVineJSをコアチームで作成、フレームワークで利用しています。
VineJSは有名なバリデーションライブラリのZodのような書き方で、VineJSの公式ドキュメントに載っているベンチマークではZodよりも高速だそうです。
AdonisJSではコントローラでバリデーションを行うために、バリデータを作成する必要があります。
node ace make:validator post
app/validators/post.tsが作成され、ここでVineJSでバリデーションルールを記述します。
import vine from '@vinejs/vine'
/**
* Validates the post's creation action
*/
export const createPostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
slug: vine.string().trim(),
description: vine.string().trim().escape()
})
)
/**
* Validates the post's update action
*/
export const updatePostValidator = vine.compile(
vine.object({
title: vine.string().trim().minLength(6),
description: vine.string().trim().escape()
})
)
コントローラでの使用例は以下のようになります。
import { HttpContext } from '@adonisjs/core/http'
import {
createPostValidator,
updatePostValidator
} from '#validators/post'
import Post from '#models/post'
export default class PostsController {
async store({ request }: HttpContext) {
const data = request.all()
const payload = await createPostValidator.validate(data)
const post = await Post.create(payload)
return response.redirect(`/posts/${post.id}`)
}
async update({ request }: HttpContext) {
const data = request.all()
const payload = await updatePostValidator.validate(data)
return response.redirect(`/posts/${post.id}`)
}
}
また省略した書き方もできます。
- const data = request.all()
- const payload = await createPostValidator.validate(data)
+ const payload = await request.validateUsing(createPostValidator)
さらに素晴らしい点としてクッキー、ヘッダー、ルートパラメータのバリデーションもできる点です。
const validator = vine.compile(
vine.object({
// Fields in request body
username: vine.string(),
password: vine.string(),
// Validate cookies
cookies: vine.object({
}),
// Validate headers
headers: vine.object({
}),
// Validate route params
params: vine.object({
}),
})
)
await request.validateUsing(validator)
さらにReact Hook Formでも公式リゾルバーが提供されているため、クライアント側のバリデーションでも流用できそうです。
Ruby on Rails
Railsではバリデーションルールはモデルに書きます。
class Person < ApplicationRecord
validates :name, presence: true
end
バリデーションはモデルのcreate
メソッド呼び出し時に実行されます。
person = Person.create(name: "John Doe")
person.valid?
# => true
Laravel
次にLaravelのバリデーションです。
Laravelでは色々な書き方ができますが、AdonisJSのように別ファイルにバリデーションルールを記述していく場合はフォームリクエストを作成します。
php artisan make:request StorePostRequest
Laravelの場合はリクエスト単位(作成リクエスト、更新リクエストなど)でバリデーションを書いていくことになるようです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
}
コントローラでの使用例はこのようになります。
public function store(StorePostRequest $request): RedirectResponse
{
// Retrieve the validated input data...
$validated = $request->validated();
// Store the blog post...
return redirect('/posts');
}
まとめ
あくまで個人的な意見になりますがまとめると、それぞれのフレームワークの特徴は以下のようになります。
- AdonisJS
- TypeScript-firstな感じがとても良く、各所で型補完が効くように作られている
- 書き方がLaravelに似た部分が多い
- Ruby on Rails
- とにかくシンプル(DRY)に書ける
- 規約(レール)に則れば効率よく開発できる(CoC)
- Laravel
- 書き方や機能の選択肢が豊富で、開発者側がある程度自由にカスタマイズできる
おわりに
同じMVCパターンのフレームワークでも言語が違うことを無視してもそれぞれ特徴を持っていることがわかりました。
機能以外に目を向けると、やはりRuby on RailsやLaravelの方がGitHubスター数が多く、国内でも採用している企業が多いです。
検索すればLaravelやRailsについての日本語の情報も多く見つかり、AdonisJSについてはほとんどありません。おそらくZennでのAdonisJSに関しての記事はこれが初でしょう笑。
ただ、AdonisJS自体は2015年に作られ、意外と歴史が古く長年開発され続け今のv6のような形になりました。
この記事でご覧いただいたようにLaravelやRailsの開発手法と似ている部分が多くあるため、そこまで学習コストをかけずにAdonisJSへ移行することができるはずです。
また言語がTypeScript一つで済むということはやはり大きな利点ですので、国内でも広まると良いなと思っています。
Discussion