🔷

Rails/LaravelエンジニアのためのフルスタックTypeScriptフレームワーク「AdonisJS」

2024/08/26に公開

はじめに

この記事では、RailsやLaravelに精通したエンジニアに向けて、フルスタックTypeScriptフレームワーク「AdonisJS」を紹介します。
AdonisJSは、これまでRuby on RailsやLaravelでの開発に慣れ親しんだエンジニアにとって、とても親しみやすいフレームワークで、どれほど似ているか比較してみようと思います。

AdonisJSとは?

AdonisJSとはTypeScriptベースのフルスタックWebフレームワークです。
MVCパターンを採用しており、Ruby on RailsやLaravelのようにルーティング、コントローラ、ORMなど公式パッケージが提供されていてエコシステムが整っているのが特徴です。
また、型安全性を重視しているのも良い点です。

https://adonisjs.com/

プロジェクト作成

AdonisJS

npm init adonisjs@latest blog

AdonisJSの場合は基本的にスターターキットを選択してプロジェクトを作成します。
テンプレートエンジン(EdgeJS)を使った昔ながらのWeb starter kitや、SPA(InertiaJS)のInertia starter kit、RailsのAPIモードのようなAPIサーバー構築用のAPI starter kitなどが選択肢にあります。

詳しくはドキュメントを参照ください。

https://docs.adonisjs.com/guides/getting-started/installation#starter-kits

Ruby on Rails

rails new blog

Railsの場合は公式で提供しているスターターキット的なものはない認識で、-m オプションを使うと自分または誰かの用意したテンプレートを適用することができます。

rails new blog -m ~/template.rb

Laravel

composer create-project laravel/laravel blog

Docker環境で開発したい場合はSailというツールも用意されています。

https://laravel.com/docs/11.x/sail

Laravel公式のスターターキットにはBreezeとJetstreamというものがあり、Breezeは基本的な機能が提供されており、Jetstreamの方はより高機能というイメージです。
プロジェクト作成時に利用するか聞かれるはずですし、後からインストールすることもできます。

https://laravel.com/docs/11.x/starter-kits

こちらもテンプレートエンジン(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拡張もあり、そこでルート一覧を確認することもできます。

https://marketplace.visualstudio.com/items?itemName=jripouteau.adonis-vscode-extension

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

https://guides.rubyonrails.org/routing.html

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]);
}

他にもさまざまな書き方ができるようなので、詳しくはドキュメントを参照してください。

https://laravel.com/docs/11.x/routing

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) {}
}

https://docs.adonisjs.com/guides/basics/controllers

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()
  }
}

https://docs.adonisjs.com/guides/basics/controllers#dependency-injection

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のようなファイルがあればレンダリングされます。
かなり省略して書けるイメージです。

https://guides.rubyonrails.org/action_controller_overview.html

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が新規登録の画面描画用であることに注意が必要です。

https://laravel.com/docs/11.x/controllers

また、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,
    ) {}
}

https://laravel.com/docs/11.x/controllers#dependency-injection-and-controllers

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

https://docs.adonisjs.com/guides/database/lucid#migrations

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

https://guides.rubyonrails.org/active_record_migrations.html

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

https://laravel.com/docs/11.x/migrations

Model

AdonisJS

AdonisJSはLucid ORMというKnexベースで作られたActive RecordパターンのORMが提供されています。

https://docs.adonisjs.com/guides/database/lucid

モデルの作成コマンドは以下のようになります。

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()

詳しくはドキュメントに書かれています。

https://lucid.adonisjs.com/docs/crud-operations

Ruby on Rails

RailsのORMはみなさんご存知のActive Recordです。

https://guides.rubyonrails.org/active_record_basics.html

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です。

https://laravel.com/docs/11.x/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というテンプレートエンジンを使用します。

https://edgejs.dev/

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でビューを構築することもできます。

https://docs.adonisjs.com/guides/views-and-templates/inertia

コントローラで以下のように書けます。

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の記述のみでインタラクティブなアプリケーションを作れるようになります。
詳しくは公式サイトを参照ください。

https://turbo.hotwired.dev/

Laravel

LaravelはテンプレートエンジンとしてBaldeを採用しています。

https://laravel.com/docs/11.x/blade

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というライブラリが提供されています。

https://laravel-livewire.com/

逆にReactやVueなどをフロントエンドに採用したい場合はAdonisJSと同じInertiaJSに対応しています(元々InertiaJSはLaravel向けにチューニングされたもの)。

https://inertiajs.com/

スターターキットの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_actionafter_actionなど)になるかなと思います。

https://guides.rubyonrails.org/action_controller_overview.html#action-callbacks

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と似たようなものになっています。

https://laravel.com/docs/11.x/middleware

まず、ミドルウェア作成コマンドです。

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をコアチームで作成、フレームワークで利用しています。

https://vinejs.dev/

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でも公式リゾルバーが提供されているため、クライアント側のバリデーションでも流用できそうです。

https://github.com/react-hook-form/resolvers?tab=readme-ov-file#vinejs

Ruby on Rails

Railsではバリデーションルールはモデルに書きます。

class Person < ApplicationRecord
  validates :name, presence: true
end

バリデーションはモデルのcreateメソッド呼び出し時に実行されます。

person = Person.create(name: "John Doe")
person.valid?
# => true

https://guides.rubyonrails.org/active_record_validations.html

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');
}

https://laravel.com/docs/11.x/validation#form-request-validation

まとめ

あくまで個人的な意見になりますがまとめると、それぞれのフレームワークの特徴は以下のようになります。

  • 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一つで済むということはやはり大きな利点ですので、国内でも広まると良いなと思っています。

Fusic 技術ブログ

Discussion