🏎️

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年連続で選出されることとなりました✨

https://zenn.dev/x_point_1/articles/ec80f7afe42b74

https://zenn.dev/x_point_1/articles/8e982be93ca26f

技術選定は未来への投資

今回の話で強調したいのは、
「TypeScript + Fastify + ROPが最強!」ということではありません。
(結構強いですがw)

未来を見据えた技術選定が、結果的にAI時代で刺さった ということです。

当時の私は、まだまだ駆け出しで、こうした選択の意義を深く理解できていたわけではありません。
ですが今振り返ると、「あの時の判断があったから今がある」と実感しますし、素直に さすがCTOだな と感じます。

そして今回この記事を書きながら、
自分もいつかは未来を見据えた選択ができるようになりたいと、改めて感じました。

めでたしめでたし👏

おわりに

弊社ではカジュアル面談を実施しています。
少しでも興味を持っていただけた方は、ぜひ以下のリンクからご連絡ください!
https://x-point-1.net/

エックスポイントワン技術ブログ

Discussion