AI時代の爆速開発を支える“刺さる”バックエンド設計。FastifyとROPが導いた答え
こんにちは、Ryotaです🐶
今や生成AIは、ソフトウェア開発に欠かせない大切なお友達🩷
コードを書くのも、テストを書くのも、なんでもかんでも補完してもらうのが当たり前になりました。
そんな時代が到来する 少し前──
弊社CTOが選んだ技術スタックが、結果的にAI時代の今、まさに “ぶっ刺さる”設計 だったんです。
今回の記事では、
- Python + AWS Lambda から
- Typescript + Fastify(ECS) + Railway Oriented Programming (ROP)
へ移行した背景と、その選択が結果的にAI時代の開発をどう加速させたのかをご紹介します✨
移行前の構成が抱えていた壁
当時のバックエンドは、PHPやPythonを中心に運用しており、特に一部は AWS Lambda(Python)で構築していました。
小規模な機能追加やスピード重視では便利だったものの、開発が進むにつれていくつかの壁にぶつかりました。
- 動的型付けのつらさ(エラーに気づくのが遅い)💣
- try-except地獄(どこでエラーがthrowされているか分かりづらい)💣
- テストのしにくさ(関心ごとの分離ができておらず複雑化)💣
- サーバーレス特有の制約で機能拡張が難しい 💣
結果的に、進めるよりも直すことに時間を割かれるようになっていました💥
移行の決断:Typescript + Fastify + ROP
こうした課題を受けて、当時CTOが選んだのが以下の技術スタックでした。
TypeScript
- コンパイル時に型エラーを検出
- フロントエンドと同じ言語で「全員フルスタック化」
Fastify
- Expressより速く、軽量
- JSON Schemaベースで型安全なAPI開発ができる
ROP (Railway Oriented Programming)
- 成功と失敗を 「線路」 で表現し、Result型で一貫したエラーハンドリング
- ネスト地獄を避け、処理の流れがパイプラインのように読みやすくなる
使用しているライブラリと実装方針
- ramda:pipe などの関数型ユーティリティ
- ts-pattern:パターンマッチング (match(...).with(...).exhaustive())
- ts-essentials:ユーティリティ型
- Result型:独自実装(必要最小限の成功/失敗のみ)
try-catch地獄からの脱出
Before (Python + Lambda)
def handler(event, context):
try:
user_id = event["pathParameters"]["userId"]
if not user_id:
return {"statusCode": 400, "body": "Bad Request"}
user = db.get_user(user_id)
if not user:
return {"statusCode": 404, "body": "User Not Found"}
return {"statusCode": 200, "body": user}
except Exception:
return {"statusCode": 500, "body": "Internal Server Error"}
After (Fastify + TypeScript + ROP + Schema由来の型)
import { pipe } from "ramda";
import { match } from "ts-pattern";
import { start, bypass, dbMiddleware } from "@/utils/pipeline";
import type { FastifyRequest, FastifyReply } from "fastify";
import type { Result } from "@/types/result";
// 👇 Schemaから生成した型を利用
import type { GetUserRequest, GetUserResponse } from "./schema";
type ExtractParamsError = { errorCode: "extractParams_400" };
type GetUserError =
| { errorCode: "getUserFromDB_404" }
| { errorCode: "getUserFromDB_500" };
type User = { id: string; name: string; email: string };
const extractParams = (
req: FastifyRequest<GetUserRequest>
): Result<{ userId: string }, ExtractParamsError> => {
const userId = (req.params as any)?.userId;
return userId
? { success: true, data: { userId } }
: { success: false, error: { errorCode: "extractParams_400" } };
};
const getUserFromDB = async (p: {
userId: string;
prisma: any;
}): Promise<Result<User, GetUserError>> => {
try {
const user = await p.prisma.user.findUnique({ where: { id: p.userId } });
if (!user) return { success: false, error: { errorCode: "getUserFromDB_404" } };
return { success: true, data: { id: user.id, name: user.name, email: user.email } };
} catch {
return { success: false, error: { errorCode: "getUserFromDB_500" } };
}
};
export const getUserHandler = async (
req: FastifyRequest<GetUserRequest>,
reply: FastifyReply
) =>
pipe(
start(extractParams(req)),
bypass(dbMiddleware(getUserFromDB)),
async (result) =>
match(await result)
.with({ success: true }, ({ data }) =>
reply.status(200).send({ user: data })
)
.with({ error: { errorCode: "extractParams_400" } }, () =>
reply.status(400).send({ message: "Bad Request: userId is required" })
)
.with({ error: { errorCode: "getUserFromDB_404" } }, () =>
reply.status(404).send({ message: "User not found" })
)
.with({ error: { errorCode: "getUserFromDB_500" } }, () =>
reply.status(500).send({ message: "Internal server error" })
)
.exhaustive()
)();
改善ポイント
-
型が保証する安心感
→ JSON Schema を元に生成した型を使うことで、API定義と実装の齟齬がなくなり、「動かしてみないと分からない」不安がなくなった。実装中から安心できるようになった。 -
パイプラインでエラーハンドリングが統一
→ 各ハンドラーごとにバラバラだった例外処理が一つの流れに収まり、コードの見通しが大幅に向上 -
.exhaustive()
でケース漏れをコンパイル時に検出
→ ヒューマンエラーが減り、レビューでの指摘も大幅に減った
これらの積み重ねによって、新機能を追加するたびに感じていたストレスが解消され、開発体験は文字通り“劇的に”改善しました✨
AI前提の時代と相性の良さ
私たちエンジニアが新しい技術スタックに慣れてきた頃、世の中に急速に普及したのが 生成AI です。
試しに簡単なAPIをAIに任せてみたところ──
ほとんど手直しなく、実用レベルのコードが完成してしまいました。
なぜこんなことが可能だったのか?
その理由は── もともとの技術スタックがAIと親和性抜群 だったからです。
- スキーマと型が揃っているから、AIが正しくコードを生成しやすい
- ROPのエラーハンドリングはシンプルでAIが自然に従える
- 小さな関数単位に分かれているから差し替え・修正も容易
つまりAIが走りやすいレールがあらかじめ敷かれていたのです‼️
まずその結果として、
- ✅ 型でエラーを実行前に検出できる
- ✅ ボイラープレートが減り、ハンドラーは半分以下の行数に
- ✅ エラーハンドリングが統一され、レビュー効率も向上
- ✅ 関数変更も型エラーで即検知できる
といった効果が得られました。
さらにそこに生成AIが加わったことにより、
- ✅ スキーマと型が揃っているため、AIが正しくコードを生成しやすい
- ✅ ROPのシンプルなエラーハンドリングはAIにとって理解しやすい
- ✅ 小さな関数単位で分割されているため、AIによる修正や差し替えも容易
と、開発スピードが一気に加速したのです 🚀
そして気づけば、この加速がチーム全体の成果につながり、「Findy Team+ Award 2025」 の「Organization Award Solution Consulting 部門」に2年連続で選出されることとなりました✨
技術選定は未来への投資
今回の話で強調したいのは、
「TypeScript + Fastify + ROPが最強!」ということではありません。
(結構強いですがw)
未来を見据えた技術選定が、結果的にAI時代で刺さった ということです。
当時の私は、まだまだ駆け出しで、こうした選択の意義を深く理解できていたわけではありません。
ですが今振り返ると、「あの時の判断があったから今がある」と実感しますし、素直に さすがCTOだな と感じます。
そして今回この記事を書きながら、
自分もいつかは未来を見据えた選択ができるようになりたいと、改めて感じました。
めでたしめでたし👏
おわりに
弊社ではカジュアル面談を実施しています。
少しでも興味を持っていただけた方は、ぜひ以下のリンクからご連絡ください!
Discussion