はじめに
こんにちは! @Yu_yukk_Yです!
先日、70チーム前後のチームが参加した大規模ハッカソンであるQiitaハッカソンの本戦があり、予選を勝ち抜いた10チームでハッカソンをしました
私たちはチーム「エニッツ」として本戦に出場し、入賞することができました
この記事ではエニッツのバックエンドを1人で担当した自分が爆速でバックエンドを作り上げるために行った工夫について書きます
この記事で紹介する工夫には営利目的でアプリを作成する場合には品質的な観点で絶対にやってはいけないような工夫も含まれます。これは筆者の「開発環境・技術構成は目的に応じて柔軟に変えるべき」という思想に基づいたものです。
実際の開発でこの記事に書かれている工夫を用いるときは、それがプロジェクトの目的に即しているものなのか、よく考えてから用いるようにしてください。
技術構成
使用した技術構成は図のような感じで、フロントエンドであるARのアプリケーションからCloud RunにデプロイしたAPIサーバを叩いてもらう構成になっています。バックエンドについてより細かく書くと下記のような感じです。
- 言語: TypeScript(Bun)
- Webフレームワーク: Hono
- ORM: Drizzle ORM
- コンピューティングサービス: Cloud Run
- DBサーバ: PlanetScale
- 認証基盤: Firebase
前提
- チームにバックエンドエンジニアは自分1人。インフラも自分が担当
- 開発は3/2 11:00 ~ 3/3 14:00の27時間という超短期間で行われた
工夫ポイント1: APIドキュメント自動生成機能を活用してコミュニケーションコストを減らす
クライアントエンジニアとのコミュニケーションコストの軽減方法としてOpenAPIを活用することは一般的な方法です。しかし、2日という短い時間で成果物を作らなければならない今回のハッカソンではswagger.yaml
を書いている時間は当然ながらありませんでした
そこで、APIドキュメントの自動生成をしました!
Honoのサードパーティ製のミドルウェアに@hono/zod-openapiというものがあり、これを使うことでHonoのrouteとリクエストボディ・レスポンスのスキーマ・バリデーションを定義しながらOpenAPI Documentのjsonを生成できます!
これと@hono/swagger-uiを組み合わせることで、ブラウザ上でAPIドキュメントを閲覧できるようになります!
例えば次のように書いて実行し
import { z, createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: 'id',
in: 'path',
},
example: '1212121',
}),
})
const UserSchema = z
.object({
id: z.string().openapi({
example: '123',
}),
name: z.string().openapi({
example: 'John Doe',
}),
age: z.number().openapi({
example: 42,
}),
})
.openapi('User')
const route = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: UserSchema,
},
},
description: 'Retrieve the user',
},
},
})
const app = new OpenAPIHono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.openapi(route, (c) => {
const { id } = c.req.valid('param')
return c.json({
id,
age: 20,
name: 'Ultra-man',
})
})
// The OpenAPI documentation will be available at /doc
app.doc('/doc', {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'My API',
},
})
app.get('/ui', swaggerUI({ url: '/doc' }))
export default app
ブラウザ上でhttp://localhost:3000/ui
を開くと次のようなAPIドキュメントが表示されます!
実装と並行してAPIドキュメントを作成できるので、クライアントエンジニアとのコミュニケーションコストを軽減しつつ、OpenAPIのyamlファイルを書く手間を無くすことができます!
工夫ポイント2: アダプター層から実装することでGithub Copilotによるコード自動生成を全力活用
今回のハッカソンでは、コード生成AIであるGithub Copilot
を最大限活用しました!
GIthub Copilotは今開いているファイルと別タブで開いているファイルの情報を学習し、コードを作成してくれます。 つまり、複雑なデータの加工を伴わないような簡単な処理であれば、入出力の型と関数名だけ定義しておけばほとんどの処理を生成できます。
今回で言うと、工夫ポイント1で作成したAPIのレスポンス・リクエストボディのスキーマのコードに加えて、Drizzle ORMを用いてDBテーブル定義もTypeScriptのコードで記述したため、必要なデータスキーマは全てTypeScriptのコードで定義済みの状態になっていました。そのため、Controllerで実際の処理を実装する際、そのほとんどをGithub Copilotに実装させることができました。
もちろん多少の手直しは必要でしたが、それでも大幅に作業時間を短縮できました!
工夫ポイント3: Bunを使用
BunはNode.js互換のJavaScriptランタイムですが、2024年3月21日現在、完全な互換性は実現できていません。
Bunの互換性の状況は下記のページから確認することができます。
https://bun.sh/docs/runtime/nodejs-apis
BunはBundler、Test Runner、Node.js互換のパッケージマネージャーを備えた、スピードを追求したJavaScriptランタイムです。
既に上記の説明でBunが優れた技術であることはご理解いただけたと思うのですが、ハッカソンにおいて特筆すべきはパッケージマネージャーとしての性能の高さです。
試しに次のpackage.json
がある環境でnpmとBunでそれぞれインストールを実行し、かかった時間を比較してみます
環境:
- Apple M1
- MacOS Ventura: 13.6.5
- Bun: 1.0.22
- npm: 10.5.0
-
node_modules
とlockファイルは都度削除して検証しました
{
"scripts": {
"dev": "bun run --hot src/index.ts"
},
"dependencies": {
"@hono/swagger-ui": "^0.2.1",
"@hono/zod-openapi": "^0.9.8",
"hono": "^4.1.0"
},
"devDependencies": {
"@types/bun": "latest"
}
}
$ npm install
added 13 packages, and audited 14 packages in 8s
1 package is looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ bun install
bun install v1.0.22 (b400b36c)
+ @types/bun@1.0.8
+ @hono/swagger-ui@0.2.1
+ @hono/zod-openapi@0.9.8
+ hono@4.1.0
13 packages installed [1.60s]
8s(npm) vs 1.6s(Bun)
この通り、爆速 です。今回はコードの実行もBunを用いているのですが、実行をNode.jsなどの他のJavaScriptランタイムに任せる場合においてもPackage Managerとして大変優秀で、使わない理由はないと思います
工夫ポイント4: Docker Compose Watchを使用
Docker Compose Watch
とは、Docker Compose
に内蔵されている機能の1つでローカルファイルの変更を検知し、コンテナ内部のファイルを置き換えたり、コンテナを再ビルドしてくれる機能です。
この機能を用いることで、開発環境のコンテナの再ビルドやコードの置き換えが自動で行われるようになり、開発効率を大きく向上させることができました
Docker Compose Watch
については以前記事を作成したため、使い方やメリットについてより詳しくは下記の記事をご覧ください↓
また、開発環境をDockerで作成し、本番環境もコンテナ実行プラットフォームであるCloud Runを使用したことで、デプロイ時に環境差分によるバグを考えなくてもよくなった点もGoodです
工夫ポイント5: CI / CDを行わない
注意
下記にも記載していますが、今回は2日間という超短期間で、かつバックエンドが1人という状況で成果を出すことが求められたためにこのような決断をしました。
開発が中長期に渡る場合や複数人で開発する場合はCI/CDの導入を検討すべきです。
開発の高速化する場合CI/CDパイプラインを整備するのが一般的です。しかし、今回のハッカソンではCI/CDパイプラインの構築を一切行わず、その分の時間を機能の開発に充てました。
理由は下記の通りです。
手動デプロイの方が手軽
一般に継続的デプロイには次のようなメリットがあると自分は考えています。
- デプロイ操作のミスの軽減
- リリースサイクルの高速化
しかし、これらのメリットは複数人で開発する場合のものであり、1人で開発する場合は結局ローカルに構築した設定を使い回すことになるため、メリットをあまり享受できません
自分はバックエンドを1人で開発していたため、CDにメリットを感じませんでした(自分の場合はgcloud run deploy
コマンドにいくつかオプションを足したものを使い回していました)。
一方、CDの今回のケースにおけるデメリットとして、ブランチ運用を前提としている点があると考えています。
もちろん、pushをトリガーにデプロイすることも技術的には可能です。しかし、その場合「とりあえずpushしときたかった」みたいなコミットすらデプロイされてしまい、コミットの粒度や質を気にしながら開発する必要が出てきます。そのため、雑にコミットしがちな短期のハッカソンとは相性が悪いです
上記のように、CDを導入することにあまりメリットが感じられず、むしろデメリットが目立つようなケースでは、手軽に行える手動デプロイの方が扱いやすく、またより機能開発に集中できる選択肢といえます
テストコードを書くメリットが薄い(= CIでテストが不要)
自分は普段、テストコードを書く習慣をとても大切にしています。それは思わぬバグに素早く気づくことができたり、リファクタが可能になったりするほか、テスタビリティを意識して実装を書くことでコードの品質を向上させることができるなど、多くのメリットが得られるからです!
しかし、2日という短い期間のハッカソンにおいて1人でバックエンドを開発する場合、これらのメリットはほとんど得られません
むしろテスト設計やコードに時間を割いてしまって実装したかった機能を実装しきれない方がよっぽど避けるべきことだと考え、テストコードを書かずに新機能の開発にその分の時間を充てました。
CIでlinter・formatterを動かすメリットが薄い
これはバックエンドを開発しているのが自分1人であり、VS Codeに自動でliter・formatterが動作するように設定をしていたからです(liter・formatterにはbiomeを使用しました)。
バックエンドのコードを書くのが1人で、かつローカルでコードが整形されるならCIによるコードのチェックは当然不要です。
そもそもセットアップに時間がかかる
2日間という短い期間で開発するハッカソンでは、1分1秒が惜しいです。そのため、何に工数を割くべきか迅速に、かつ慎重に決める必要があります。
上記の通り、今回のハッカソンではCI/CDを整備するメリットよりその時間をより多くの機能を開発することに尽すべきだと考え、CI/CDを構築しないという選択をとりました
まとめ
今回のハッカソンでは上記のような工夫を凝らしたことでバックエンドの開発を爆速で進めることができました
この記事に記した工夫が誰かの参考になると嬉しいです