BunのWebフレームワークのElysiaJSの基礎を学ぶ

ElysiaJS(エリシア)は Bun 用の Web フレームワークです。Express と比較して 18 倍高速に動作するとElysiaJS のホームページには掲載されているためどのようなフレームワークなのか気になっている人もいるかと思います。本文書は Bun の入門者で ElyaisJS がどのようなものか基本的な機能/設定を理解したいという人向けの内容になっています。
動作確認は macOS で行っています。
Bun のインストールか基本的な利用方法については下記の文書で公開しているので参照してください。
目次
プロジェクトの作成

プロジェクトの作成は bun create elysia コマンドで行います。bun create elysia コマンドの後には任意の名前のプロジェクト名をつけることができます。ここでは bun-elysia という名前で設定を行っています。
% bun create elysia bun-elysia
% bun install
bun install v1.0.7 (b0393fba)
🔍 Resolving [1/2]
[94.00ms] git
+ bun-types@1.0.7
+ elysia@0.7.18
9 packages installed [1471.00ms]
[1491.00ms] bun install
[2.22s] bun create elysia
Come hang out in bun's Discord: https://bun.sh/discord
-----
A local git repository was created for you and dependencies were installed automatically.
Created elysia project successfully
# To get started, run:
cd bun-elysia
bun run src/index.ts
プロジェクト作成後はプロジェクトディレクトリ bun-elysia に移動します。package.json ファイルを確認して scripts とインストールしたパッケージを確認します。
{
"name": "bun-elysia",
"version": "1.0.50",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts"
},
"dependencies": {
"elysia": "latest"
},
"devDependencies": {
"bun-types": "latest"
},
"module": "src/index.js"
}
アプリケーションを起動することに関係するので scripts に dev が登録が設定されていることを確認しておきます。
src ディレクトリの中を確認すると index.ts ファイルがあるので内容を確認します。コードはシンプルで get メソッドでルートを定義して listen メソッドでアプリケーションのポートを設定しています。
import { Elysia } from "elysia";
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
package.json に記載されている scripts の dev を利用してアプリケーションを起動します。
% bun run dev
$ bun run --watch src/index.ts
🦊 Elysia is running at localhost:3000
ブラウザから http://localhost:3000 にアクセスすると以下の画面が表示されます。

API のエンドポイントとして利用していくのでブラウザではなくツールを利用して動作確認を行っていきます。本文書ではエディターに VS Code を利用しているので Extensions の Thunder Client を利用します。
Thunder Client の利用は必須ではないので Postman や同じく VS Code の Extensions の REST Client など好きなツールを利用してください。

Thunder Client を利用して http://localhost:3000 に GET リクエストを送信する場合は GET にアクセスする URL を設定して”Send”ボタンをクリックします。リクエストの結果は右側に表示され Response として”Hello Elysia”、Status コードが 200 で戻されていることがわかります。

ルートの設定
Elysia での ルート の設定方法について確認を行っていきます。ルートの設定はメソッド名の引数にルートのパス、callback 関数、オプションで hook を指定します。
.[method name](path, callback, hook?)
デフォルトの index.ts でのルートの設定には hook がありませんがパスと callback 関数が設定されています。これが基本的なルートの設定で定義したパスにリクエストがあった場合に callback 関数が実行されます。
.get("/", () => "Hello Elysia")
Path Parameters
ルートの定義に:(コロン)をつけることで URL に含まれる動的な値を変数として取得することができます。id は callback 関数の引数に含まれるオブジェクトの params.id で取得することができます。
import { Elysia } from 'elysia';
const app = new Elysia()
.get('/:id', ({ params: { id } }) => `id: ${id}`)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
http://localhost:3000/の後には任意の文字列を設定することができます。下記では 99 を設定しています。

メソッド
ここまではルートの設定では get メソッドのみを利用してきましたが post, put, patch, delete などのメソッドを設定することができます。クライアントから POST リクエストが送信する場合は post メソッドを利用して送信されてきたデータを取得することができます。下記のコードは取得した body の値を何も加工せずにそのままクライアントに戻しています。
import { Elysia } from 'elysia';
const app = new Elysia().post('/users', ({ body }) => body).listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Thunder Client を利用している場合先ほどまで URL の横に指定していたのは GET でしたが送信するリクエストによって値を変更することができます。POST リクエストを送信する場合は GET ではなく POST を選択します。POST リクエストではデータ送信を行うので Body タブに切り替えて JSON タブで JSON データを設定して”Send”ボタンをクリックします。右側の Response タブ に送信したデータが Status コード 200 と共に表示されることが確認できます。

ElysiaJS 側の post メソッドの callback 関数の中で受信したオブジェクトデータ body をそのまま戻り値としていますが Content-Type が application/json として戻されます。Thunder Client の右側の Headers タブを選択すると Response Headers の Content-Type が application/json に設定されていることでも確認できます。

Content-Type を設定していないにも関わらず application/json に設定されているのは Elysia が callback 関数の戻り値の構造によって Content-Type の設定と戻すデータに関する処理を自動で行ってくれているためです。Elysia に任せずに Response を利用して記述すると下記のようになります。
const app = new Elysia()
.post(
'/users',
({ body }) =>
new Response(JSON.stringify(body), {
headers: { 'Content-Type': 'application/json' },
})
)
.listen(3000);
“Hello World”のように文字列を戻した場合には Content-Type は text/plain;charset=utf-8 が設定されます。

Handler について
動作確認した app.get, app.post の第二引数は Handler と呼ばれる Response を戻すための関数です。Handler の callback 関数の引数には Context が渡されています。Context の中にはさまざまな情報が含まれており、params.id や body は Context に含まれる情報の一部です。どのような情報が含まれているか下記のコードを利用して確認してみましょう。
const app = new Elysia().get('/:id', (context) => context);
Thunder Client から設定した URL に対して GET リクエストを送信し、送信後に戻される値を確認すると request, store, qi, set, params などの含まれていることがわかります。

params が含まれていることが確認できるので以下のコードのように context の中から id を取り出すことが可能になります。
const app = new Elysia()
.get('/:id', ({ params: { id } }) => `id: ${id}`)
.listen(3000);
Context の中に set の中に status が含まれていましたが Response で戻す status コードを変更したい場合には set を利用して行うことができます。set.status で 201 を設定しているのでクライアントに戻される Status コードはデフォルトの 200 ではなく 201 となります。
const app = new Elysia()
.get('/:id', ({ params: { id }, set }) => {
set.status = 201;
return `id: ${id}`;
})
.listen(3000);
バリデーションの設定
@sinclair/typebox パッケージの TypeBox を利用してスキーマを定義することで POST リクエストから送信されてくる body のバリデーションを行うことができます。TypeBox でスキーマを定義することで JSON Schema を作成して 、定義したスキーマから型を取得することができます。
スキーマ定義
どのようにスキーマを定義してバリデーションを行なっていくのか簡単なコードを利用して確認していきます。
スキーマの設定はルートの定義の hook の箇所で行っています。
.[method name](path, callback, hook?)
下記のコードが最もシンプルなコードです。
import { Elysia, t } from 'elysia';
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: t.Object({
name: t.String(),
email: t.String(),
}),
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
elysia から import している”t”が@sinclair/typebox で t.Object の部分が TypeBox を利用したスキーマの定義です。定義したスキーマでは body に含まれるデータは name, email プロパティを持ち、それぞれの値が文字列であることを定義しています。
t.Object({
name: t.String(),
email: t.String(),
}
スキーマを定義する前の状態ではエディター上で body の上にカーソルを当てると body の型には unknown が設定されています。

スキーマを定義すると body の型が設定されていることが確認できます。

バリデーションの動作確認
スキーマを定義後にバリデーションが行われるのか確認するために Thunder Client を利用して POST リクエストを送信します。送信するデータには name プロパティが含まれていないためバリデーションにエラーが発生してステータスコード 400 Bad Request とメッセージが戻されていることが確認できます。

name プロパティが存在しない場合にエラーが発生する理由は定義したスキーマでは下記のような JSON Schema が設定されているためです。
const T = {
type: 'object',
required: ['name', 'email'],
properties: {
name: {
type:'string'
},
email: {
type:'string'
}
}
}
name プロパティが必須ではない場合には下記のように記述することができます。 送信するデータに name プロパティが含まれていなくてもエラーが発生することはありません。
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: t.Object({
name: t.Optional(t.String()),
email: t.String(),
}),
})
.listen(3000);

バリデーションでは送信されるデータの存在か型のチェックだけではなく送信されているデータの中身のバリデーションも行うことができます。例えば name の文字列の制限を行うことも可能です。最低 3 文字が必要である場合は minLength を利用することができます。
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: t.Object({
name: t.String({
minLength:3
}),
email: t.String(),
}),
})
.listen(3000);
name の値が 2 文字で送信すると 400 Bad Request が戻されます。3 文字以上にするとエラーはなくなります。

リクエスト body に対するスキーマの定義とバリデーションをどのように行うか理解ができたかと思います。
その他のバリデーション
送信されてくる body に対してバリデーションを行ってきましたが body 以外にもバリデーションを行うことができます。
- body
- query
- params
- header
- response
バリデーションの設定方法は body と同じなので response を使って方法を確認します。response であれば Handler からクライアント側に戻すデータに対してバリデーションを行うことができます。
動作確認のため response にも body と同じスキーマを設定します。Response にも name と email が含まれている必要がありますが body の email のみ戻すように設定を行います。
const app = new Elysia()
.post('/users', ({ body }) => body.email, {
body: t.Object({
name: t.String(),
email: t.String(),
}),
response: t.Object({
name: t.String(),
email: t.String(),
}),
})
.listen(3000);
コードを記述した時点でエディターにより Response で戻される型と戻すデータの型が異なるためエラーメッセージが表示されます。

エディター上でのエラーを無視して POST リクエストを送信すると 400 Bad Request のエラーが戻されます。

body, response に関するバリデーションの設定を確認しましがその他についても同様の方法で行うことができます。
モデルの設定
body と response のように同じスキーマの設定を行っている場合には変数としてモデルを定義して設定することができます。
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: t.Object({
name: t.String(),
email: t.String(),
}),
response: t.Object({
name: t.String(),
email: t.String(),
}),
})
.listen(3000);
body と response で設定しているスキーマの定義は共通なので 1 つの変数として定義します。定義した変数 userDTO を body と response に設定します。
const userDTO = t.Object({
name: t.String(),
email: t.String(),
});
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: userDTO,
response: userDTO,
})
.listen(3000);
同じファイルではなく別ファイルの models.ts ファイルを作成してその中にモデルの情報を記述して import することもできます。
import { t } from 'elysia';
export const userDTO = t.Object({
name: t.String(),
email: t.String(),
});
作成した userDTO を index.ts ファイルで import します。
import { Elysia } from 'elysia';
import { userDTO } from './models';
const app = new Elysia()
.post('/users', ({ body }) => body, {
body: userDTO,
response: userDTO,
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
スキーマに対して名前をつけて setModel(model メソッド) で設定を行うことができます。model メソッドの引数にはキーと値を持つオブジェクトで設定を行います。ここではスキーマ定義に対して user という名前のキーをつけて設定しています。body と response には設定した user キーを指定します。
import { Elysia, t } from 'elysia';
const app = new Elysia()
.model({
user: t.Object({
name: t.String(),
email: t.String(),
}),
})
.post('/users', ({ body }) => body, {
body: 'user',
response: 'user',
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
さらにプラグインとして利用するため model のメソッド部分を別ファイルに設定することもできます。新たに models ディレクトリを作成して user 用のモデルファイル user.model.ts ファイルを作成します。
import { Elysia, t } from 'elysia';
export const userModel = new Elysia().model({
user: t.Object({
name: t.String(),
email: t.String(),
}),
});
作成した userModel はプラグインとして use メソッドの引数に設定することで登録することができます。
import { Elysia, t } from 'elysia';
import { userModel } from './models/user.model';
const app = new Elysia()
.use(userModel)
.post('/users', ({ body }) => body, {
body: 'user',
response: 'user',
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Plugins の設定については別の例で後ほど説明を行っています。

user.model.ts ファイルでは user キーも持つスキーマ定義を一つ設定していましたが setModel のオブジェクトでは複数のキーでスキーマ定義を設定できるので response 用に email をキーにもつスキーマを定義します。
import { Elysia, t } from 'elysia';
export const userModel = new Elysia().model({
user: t.Object({
name: t.String(),
email: t.String(),
}),
email: t.String(),
});
user と追加した email キーをそれぞれ body, response に設定することができます。
const app = new Elysia()
const app = new Elysia()
.use(userModel)
.post('/users', ({ body }) => body.email, {
body: 'user',
response: 'email',
})
.listen(3000);
データベース
アプリケーションの構築する場合はデータを保管する必要があり通常はデータベースを利用します。Bun ではデフォルトで SQLite データベースの操作を行うことができるので SQLite データベースを利用して動作確認を行います。
データベースの作成
Elysia からデータベースを操作する前に事前準備としてデータベースファイルの作成とテーブルの作成を行います。src ディレクトリに db ディレクトリを作成してその下に createDB.ts ファイルを作成します。1 件ユーザ情報も登録しておきます。
import { Database } from 'bun:sqlite';
const db = new Database('mydb.sqlite', { create: true });
db.run(
`CREATE TABLE IF NOT EXISTS users (id Integer Primary Key Autoincrement, name Text, email Text Unique)`
);
const insertUser = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
insertUser.run('John Doe', 'john@example.com');
db.close();
bun run コマンドで作成した createDB.ts ファイルを実行します。実行するとプロジェクトディレクトリ直下に mydb.sqlite ファイルが作成されます。
% bun run src/db/createDB.ts
ユーザ一覧の取得
/users のルートを定義して Handler の callback 関数の中でデータベースにアクセスしてユーザの一覧を取得し、取得したユーザ一覧をクライアントに戻します。
import { Elysia, t } from 'elysia';
import { Database } from 'bun:sqlite';
const db = new Database('mydb.sqlite');
const app = new Elysia()
.get('/users', () => {
const query = db.prepare('SELECT * from users');
return query.all();
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Thunder Client を利用して http://localhost:3000/users に GET リクエストを送信するとユーザ一覧が配列で取得できます。戻されるデータはデータベース作成時に登録した 1 件のユーザ情報です。
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
]
response にスキーマ定義を行った場合の設定方法を確認します。response は配列なので t.Array を利用して下記のように記述することができます。
const app = new Elysia()
.get(
'/users',
() => {
const query = db.prepare('SELECT * from users');
return query.all();
},
{
response: t.Array(
t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
})
),
}
)
.listen(3000);
response にスキーマの定義を行うと query.all()の型が unknown になるため型キャストを行うためスキーマから型を取得します。Static を利用することでスキーマ定義から型を取得することができます。
const userDTO = t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
});
type User = Static<typeof userDTO>;
上記のコードを利用して先ほどのコードを書き換えます。TypeScript のエラーの解消し GET リクエストでユーザ情報を取得することができます。
import { Elysia, Static, t } from 'elysia';
import { Database } from 'bun:sqlite';
const db = new Database('mydb.sqlite');
const userDTO = t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
});
type User = Static<typeof userDTO>;
const app = new Elysia()
.get(
'/users',
() => {
const query = db.prepare('SELECT * from users');
return query.all() as User[];
},
{
response: t.Array(userDTO),
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
ユーザの登録
POST リクエストでユーザデータを送信することでデータベースにユーザの登録を行います。ここでは users テーブルの id は自動採番されるので name と email を POST リクエストで送信します。
body のバリデーションでは name と email のみチェックを行うので t.Omit で userOTD から id を消去しています。Response では登録したユーザ情報をデータベースから取得して戻しています。
body: t.Omit(userDTO, ['id']),
const app = new Elysia()
.get(
'/users',
() => {
return db.prepare('SELECT * from users').all() as User[];
},
{
response: t.Array(userDTO),
}
)
.post(
'/users',
({ body }) => {
const query = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
query.run(body.name, body.email);
return db
.prepare('SELECT * FROM users WHERE email = ?')
.get(body.email) as User;
},
{
body: t.Omit(userDTO, ['id']),
response: userDTO,
}
)
.listen(3000);
Thunder Client で POST リクエストを送信すると送信したデータが戻されることが確認できます。データベースから取得しているので id には 2 が設定されています。

エラーについて
users テーブルでは email は Unique 制約を設定しているので同じ email を登録することはできません。
先ほど登録した email と同じ値を POST リクエストで送信した場合には statu コードは 500 Internal Server Error が戻され、メッセージには constraint failed が表示されます。
response のスキーマの型は Status コードによって別々に定義することができるようで下記のように設定することができます。エラーが発生した場合は set を利用して status を 400 に設定しています。
.post(
'/users',
({ body, set }) => {
try {
const query = db.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
);
query.run(body.name, body.email);
return db
.prepare('SELECT * FROM users WHERE email = ?')
.get(body.email) as User;
} catch (error: any) {
set.status = 400;
return `${error.name} ${error.message}`;
}
},
{
body: t.Omit(userDTO, ['id']),
response: {
200: userDTO,
400: t.String(),
},
}
)
POST リクエストを送信した場合は 400 の Bad Request と設定したエラー名とエラーメッセージが表示されます。

State and Decorate
Handler に対して何か値を渡したい場合に State と Decorate を利用することができます。
state を利用して渡した値は Context.store からアクセスすることができ、decorate で渡したあたいには Context からアクセスすることができます。
まずはドキュメントに記載の例を確認します。
const app = new Elysia()
.state('version', 1)
.decorate('getDate', () => Date.now())
.get(
'/version',
({ getDate, store: { version } }) => `${version} ${getDate()}`
)
.listen(3000);
state を使って登録した version と値は Context.store.version でアクセスを行い、decorate を使って登録した getDate は Context からアクセスしています。
localhost:3000/version に GET リクエストを送信すると version の値は 1 と UNIXtimestamp の現在時刻が戻されます。
ドキュメントには例として database connection を渡す場合に利用できると記載があるので今回のシンプルなコードでは有益ではありませんが db を decorate を利用して渡してみます。decorate で渡した db_connection を利用してデータベースに対する操作を行っています。
import { Elysia, Static, t } from 'elysia';
import { Database } from 'bun:sqlite';
const db_connection = new Database('mydb.sqlite');
const userDTO = t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
});
type User = Static<typeof userDTO>;
const app = new Elysia()
.decorate('db', db_connection)
.get(
'/users',
({ db }) => {
return db.prepare('SELECT * from users').all() as User[];
},
{
response: t.Array(userDTO),
}
)
.post(
'/users',
({ body, set, db }) => {
try {
const query = db.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
);
query.run(body.name, body.email);
return db
.prepare('SELECT * FROM users WHERE email = ?')
.get(body.email) as User;
} catch (error: any) {
set.status = 400;
return `${error.name} ${error.message}`;
}
},
{
body: t.Omit(userDTO, ['id']),
response: {
200: userDTO,
400: t.String(),
},
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
database の connection だけではなく class を作成してインスタンスを decorate に渡すといったことも可能です。
import { Elysia, Static, t } from 'elysia';
import { Database } from 'bun:sqlite';
const userDTO = t.Object({
id: t.Number(),
name: t.String(),
email: t.String(),
});
export type User = Static<typeof userDTO>;
class SQLiteDatabase {
db: Database;
constructor() {
this.db = new Database('mydb.sqlite', { create: true });
}
getUsers() {
return this.db.prepare('SELECT * from users').all() as User[];
}
}
const app = new Elysia()
.decorate('db', new SQLiteDatabase())
.get(
'/users',
({ db }) => {
return db.getUsers();
},
{
response: t.Array(userDTO),
}
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Plugins
プラグインを利用することでロジックをより小さな部分に分割し、サーバー全体で再利用可能なコンポーネントを定義することができます。
例えば以下のコードをプラグインを利用した場合にどのように記述できるか確認します。
import { Elysia } from 'elysia';
const app = new Elysia()
.get('/users', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
return users;
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
src ディレクトリに routes ディレクトリを作成して users.ts ファイルを作成します。users.ts ファイルには/users のルート定義の部分のみのロジックを記述します。
import { Elysia } from 'elysia';
export const userRoute = new Elysia().get('/users', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
return users;
});
作成した userRoute を index.ts ファイルで import して use メソッドの引数に設定することでプラグインとして登録することができます。
import { Elysia } from 'elysia';
import { userRoute } from './routes/users';
const app = new Elysia().use(userRoute).listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Group
下記のコードのように/api という同じ prefix を持っているルーティングを持っている場合に Group を設定することができます。
import { Elysia } from 'elysia';
const users = [
{
id: 1,
name: 'John Doe',
email: 'john@examle.com',
},
];
const app = new Elysia()
.get('/api', () => 'Hello World')
.get('/api/users', () => users)
.get('/api/users/:id', ({ params: { id } }) =>
users.find((user) => user.id === parseInt(id))
)
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
複数のルーティングで設定されている/api を Group を利用して 1 つにまとめることができます。
const app = new Elysia()
.group('/api', (app) =>
app
.get('/', () => 'Hello World')
.get('/users', () => users)
.get('/users/:id', ({ params: { id } }) =>
users.find((user) => user.id === parseInt(id))
)
)
.listen(3000);
Guard
バリデーションを行うことができるスキーマ定義はルートの hook の箇所に設定を行いましたが Guard は hook の内容が重複している場合にまとめることができます。
import { Elysia, t } from 'elysia';
const app = new Elysia()
.post('/signUp', ({ body }) => body, {
body: t.Object({
email: t.String(),
password: t.String(),
}),
})
.post('/signIn', ({ body }) => body, {
body: t.Object({
email: t.String(),
password: t.String(),
}),
})
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
Guard を利用することで下記のように記述することができます。
const app = new Elysia()
.guard({
body: t.Object({
email: t.String(),
password: t.String(),
}),
})
.post('/signUp', ({ body }) => body)
.post('/signIn', ({ body }) => body)
.listen(3000);
Elysia でもまだまだいろいろな機能を持っているのでぜひ興味を持った人はドキュメントを参考にしながら動作確認を行ってみてください。