Skip to content

feat: fastify#90

Merged
waltergalvao merged 5 commits intomainfrom
feat/fastify
Feb 23, 2026
Merged

feat: fastify#90
waltergalvao merged 5 commits intomainfrom
feat/fastify

Conversation

@waltergalvao
Copy link
Copy Markdown
Contributor

@waltergalvao waltergalvao commented Feb 23, 2026

Greptile Summary

This PR migrates the API from Express to Fastify. The migration includes updating all routers to Fastify's plugin pattern, replacing Express middleware with Fastify equivalents, and updating dependencies.

Key changes:

  • Replaced Express with Fastify 5.7.4 and migrated all routers to FastifyPluginAsync pattern
  • Updated middleware: GitHub and Slack webhook validation now uses Fastify's preHandler hooks
  • Raw body handling configured with fastify-raw-body plugin for webhook signature verification (Stripe, GitHub, Slack)
  • BullBoard authentication migrated to Fastify scoped plugin with rate limiting
  • Error handling updated to use Fastify-specific Sentry integration
  • GraphQL Yoga integration maintained with custom multipart parser bypass

Issues found:

  • The session.url! non-null assertion in stripe.router.ts could cause issues if Stripe returns null (already noted in previous threads)

Confidence Score: 4/5

  • Safe to merge with one minor issue already flagged in previous reviews
  • The migration is well-executed with proper patterns throughout. The only concern is the non-null assertion on session.url! in the Stripe router which was already noted in previous threads. All webhook validations are properly implemented, rawBody configuration is correct, and the middleware migrations follow Fastify best practices.
  • apps/api/src/app/billing/stripe.router.ts (non-null assertion on session.url)

Important Files Changed

Filename Overview
apps/api/src/fastify.ts New Fastify app setup with proper plugin registration, rawBody config, and CORS
apps/api/src/index.ts Updated to use Fastify instead of Express with proper shutdown handling
apps/api/src/app/billing/stripe.router.ts Converted to Fastify plugin with rawBody support, contains non-null assertion on redirect
apps/api/src/app/github/middlewares/validate-webhook.middleware.ts Fixed debug code (removed && 0), properly migrated to Fastify middleware pattern
apps/api/src/bull-mq/bull-board.router.ts Migrated to Fastify with scoped rate limiting and basic auth within plugin scope

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Fastify App] --> B[Plugins Registration]
    B --> C[CORS Plugin]
    B --> D[Raw Body Plugin]
    B --> E[Form Body Plugin]
    B --> F[Content Type Parser]
    
    A --> G[Router Registration]
    G --> H[Health Router]
    G --> I[GitHub Router]
    G --> J[Stripe Router]
    G --> K[Slack Router]
    G --> L[BullBoard Router]
    G --> M[Deployments Router]
    G --> N[GraphQL Yoga]
    
    I --> O[Webhook Validation Middleware]
    O --> P[Signature Verification]
    
    J --> Q[Raw Body Config]
    Q --> R[Stripe Webhook Handler]
    Q --> S[Checkout Handler]
    
    K --> T[Slack Webhook Validation]
    T --> U[Timestamp Check]
    T --> V[Signature Verification]
    
    L --> W[Scoped Plugin]
    W --> X[Rate Limiting]
    W --> Y[Basic Auth]
    W --> Z[BullBoard UI]
    
    A --> AA[Error Handlers]
    AA --> AB[Sentry Integration]
    AA --> AC[Custom Error Handler]
Loading

Last reviewed commit: 0ac92b0

@sweetr-dev sweetr-dev Bot added the large Large PR - Consider splitting up into smaller PRs to reduce risk and review time label Feb 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

Walkthrough

Migrates the API from Express to Fastify: replaces Express dependencies with Fastify equivalents, converts routers and middleware to Fastify plugins and request/reply semantics, introduces a Fastify app bootstrap, and updates error handling and Sentry integration.

Changes

Cohort / File(s) Summary
Package Dependencies
apps/api/package.json, package.json
Replaced Express ecosystem with Fastify: removed @bull-board/express, express, express-rate-limit, and AJV override; added @bull-board/fastify, fastify, fastify-raw-body, @fastify/cors, @fastify/formbody, @fastify/rate-limit.
App bootstrap
apps/api/src/express.ts, apps/api/src/fastify.ts, apps/api/src/index.ts
Deleted Express bootstrap and added Fastify buildApp() plus updated start logic, HTTPS options, route registrations, and graceful shutdown wiring.
Router conversions
apps/api/src/app/billing/stripe.router.ts, apps/api/src/app/deployment/deployments.router.ts, apps/api/src/app/github/github.router.ts, apps/api/src/app/health/health.router.ts, apps/api/src/app/integrations/slack/slack.router.ts, apps/api/src/bull-mq/bull-board.router.ts
Converted Express Router exports to FastifyPluginAsync; replaced Router handlers with fastify.get/post handlers, switched to request/reply, adapted headers/body/rawBody usage, and applied Fastify-specific registration options (preHandler/rawBody).
Webhook validation middleware
apps/api/src/app/github/middlewares/validate-webhook.middleware.ts, apps/api/src/app/integrations/slack/middlewares/validate-webhook.middleware.ts
Changed middleware signatures from (req, res, next) to (req: FastifyRequest, reply: FastifyReply) returning Promise; replaced next/error flows with reply.code().send(), adjusted header keys and rawBody usage, and updated time/signature checks.
Error handling & helpers
apps/api/src/lib/fastify-helpers.ts, apps/api/src/lib/sentry.ts
Removed Express-specific types/wrappers (RouteHandler, catchErrors), updated errorHandler to Fastify types and reply usage, and renamed exported Sentry helper to setupFastifyErrorHandler.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • sweetrdev
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "feat: fastify" is vague and generic, using only the framework name without describing the key change of migrating from Express to Fastify. Consider a more specific title like "feat: migrate API from Express to Fastify" to better convey the primary change to reviewers scanning the history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly explains the migration from Express to Fastify, detailing framework changes, architectural decisions, and specific modifications across routers and middleware.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/fastify

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/api/src/bull-mq/bull-board.router.ts Fixed
Comment thread apps/api/src/app/github/middlewares/validate-webhook.middleware.ts Fixed
Comment thread apps/api/src/app/github/middlewares/validate-webhook.middleware.ts Fixed
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/bull-mq/bull-board.router.ts Outdated
Comment thread apps/api/src/bull-mq/bull-board.router.ts Outdated
Comment thread apps/api/src/app/github/middlewares/validate-webhook.middleware.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 16 files

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/api/src/lib/sentry.ts (1)

52-52: Sentry.setupFastifyErrorHandler is valid — be aware of default error-capture noise

Sentry.setupFastifyErrorHandler is the official API for registering a Fastify error handler, and the re-export pattern mirrors what was done for Express, so the change itself is correct.

However, a known behaviour is that setupFastifyErrorHandler reports all errors by default, including handled 4xx responses (validation failures, 404s, etc.), producing significant Sentry noise. The shouldHandleError option — which lets you control which errors get captured — is only available from version 9.9.0+, while the project currently pins @sentry/node at 9.3.0. If noise becomes a problem before an upgrade, a thin wrapper around setupFastifyErrorHandler that installs a custom setErrorHandler on the Fastify instance (filtering below 500) is the available workaround.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/lib/sentry.ts` at line 52, Sentry.setupFastifyErrorHandler is a
valid re-export but with `@sentry/node` pinned to 9.3.0 you cannot use the
shouldHandleError option (added in 9.9.0) so Sentry will capture handled 4xx
noise; either upgrade the dependency to `@sentry/node` >= 9.9.0 and pass a
shouldHandleError predicate to Sentry.setupFastifyErrorHandler, or keep the
current version and replace the direct re-export by creating a thin wrapper that
calls Sentry.setupFastifyErrorHandler but also installs a Fastify
setErrorHandler on the server to filter out errors with statusCode < 500 before
forwarding to Sentry (reference Sentry.setupFastifyErrorHandler and the Fastify
setErrorHandler you will wrap).
apps/api/src/bull-mq/bull-board.router.ts (1)

27-38: The request.url.startsWith guard may be redundant.

Since this onRequest hook is scoped to the bullBoardRouter plugin and the only routes registered here are the Bull Board routes (under env.BULLBOARD_PATH), the URL prefix check on line 28 is effectively always true. It's not harmful, but it adds a misleading condition that suggests other routes might exist in this scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/bull-mq/bull-board.router.ts` around lines 27 - 38, The
onRequest hook in fastify.addHook("onRequest", ...) contains a redundant guard
checking request.url.startsWith(env.BULLBOARD_PATH) because this hook is already
registered inside the bullBoardRouter plugin whose routes are limited to that
path; remove the condition and its early return so the hook always performs
authentication for requests handled by this router (keep the auth(request.raw)
call, env.BULLBOARD_USERNAME/env.BULLBOARD_PASSWORD checks, and the
reply.header/reply.code(401).send logic intact).
apps/api/src/fastify.ts (1)

51-59: GraphQL Yoga context type cast.

The as never cast on line 58 suppresses type checking for the server context. This works but hides potential type mismatches between what Yoga expects and what's provided. Consider defining a proper server context type for the Yoga instance to ensure type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/fastify.ts` around lines 51 - 59, The handler currently silences
type checking by casting the context to `never` when calling
`yoga.handleNodeRequestAndResponse`; remove the `as never` and instead define a
proper server context type (e.g. `ServerContext`) that matches what your Yoga
instance expects, update the Yoga creation to use that generic (e.g.
`createYoga<ServerContext>(...)` or the equivalent in your codebase), and pass
an object typed as `ServerContext` into `yoga.handleNodeRequestAndResponse` in
the `app.route` handler so `app.route`, `yoga.graphqlEndpoint`, and
`yoga.handleNodeRequestAndResponse` all use the correct context type and the
compiler can validate it.
apps/api/src/index.ts (1)

32-37: Graceful shutdown can throw without being caught.

If closeAllQueueWorkers() or app.close() throws, the error becomes an unhandled rejection since shutdownGracefully is invoked from signal handlers with no .catch(). Consider wrapping in try/catch so the process still exits.

Proposed fix
   const shutdownGracefully = async (signal: string) => {
     console.log(`🔶 Received ${signal}, closing server..`);
-    await closeAllQueueWorkers();
-    await app.close();
-    process.exit(0);
+    try {
+      await closeAllQueueWorkers();
+      await app.close();
+    } catch (err) {
+      console.error("Error during graceful shutdown", err);
+    }
+    process.exit(0);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/index.ts` around lines 32 - 37, shutdownGracefully currently
awaits closeAllQueueWorkers() and app.close() without error handling, which can
create unhandled rejections when invoked from signal handlers; wrap the body of
shutdownGracefully in a try/catch/finally: in try await closeAllQueueWorkers()
and await app.close(); in catch log the error (include context and the caught
error) using the existing logger or console.error; in finally ensure
process.exit(0) on success or process.exit(1) on error so the process always
exits. Reference shutdownGracefully, closeAllQueueWorkers, and app.close when
applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/billing/stripe.router.ts`:
- Around line 70-73: Replace the non-idiomatic Fastify 5 redirect usage
`reply.code(303).redirect(session.url!)` with the Fastify 5 recommended form by
passing the status code as the second argument to redirect; update the call in
the handler that returns the Stripe checkout session (the code that currently
returns `reply.code(303).redirect(session.url!)`) to use
`reply.redirect(session.url!, 303)` so the redirect status is explicit and
follows Fastify 5 conventions.

In `@apps/api/src/app/github/middlewares/validate-webhook.middleware.ts`:
- Line 17: The early-return condition "if (!isLive && 0) return;" disables the
intended dev-skip logic; change this to "if (!isLive) return;" in the
validate-webhook middleware (the line currently containing isLive check) so
webhook validation is skipped in non-live environments as intended, and update
or remove any misleading inline comment accordingly.

In
`@apps/api/src/app/integrations/slack/middlewares/validate-webhook.middleware.ts`:
- Around line 17-18: The Slack middleware currently reads req.headers into
signature and timestamp without null-safety which can cause
Buffer.from(undefined) or NaN timestamp checks; update the code in
validate-webhook.middleware.ts to default the header reads (e.g., const
signature = req.headers["x-slack-signature"] as string || "" and const timestamp
= req.headers["x-slack-request-timestamp"] as string || ""), and then explicitly
validate presence/format (if signature === "" or Number(timestamp) is NaN or the
timestamp is too old, return a 400/unauthorized) before calling Buffer.from(...)
or performing Math.abs(time - Number(timestamp)) checks so the middleware never
throws on missing headers.

In `@apps/api/src/index.ts`:
- Line 13: Remove the stray debug console.log that prints the certificate path
(the call using console.log(resolve(__dirname, "../../../certs/tls.key")) in
apps/api/src/index.ts); either delete the console.log entirely or replace it
with a proper logger call gated by a debug flag (e.g., use your project's logger
at startup or check NODE_ENV/DEBUG before logging) so cert paths are not
unconditionally printed in production.

In `@apps/api/src/lib/fastify-helpers.ts`:
- Around line 28-30: The fastify error handler currently calls
Error.captureStackTrace(error) before captureException(error), which overwrites
the original throw-site stack sent to Sentry; change the order so
captureException(error) is called before Error.captureStackTrace(error), or
remove the Error.captureStackTrace call entirely, and keep the existing
logger.error("Fastify error handler", error) and captureException(error) calls
intact (refer to logger.error, Error.captureStackTrace, and captureException in
the handler to locate the code).

---

Nitpick comments:
In `@apps/api/src/bull-mq/bull-board.router.ts`:
- Around line 27-38: The onRequest hook in fastify.addHook("onRequest", ...)
contains a redundant guard checking request.url.startsWith(env.BULLBOARD_PATH)
because this hook is already registered inside the bullBoardRouter plugin whose
routes are limited to that path; remove the condition and its early return so
the hook always performs authentication for requests handled by this router
(keep the auth(request.raw) call, env.BULLBOARD_USERNAME/env.BULLBOARD_PASSWORD
checks, and the reply.header/reply.code(401).send logic intact).

In `@apps/api/src/fastify.ts`:
- Around line 51-59: The handler currently silences type checking by casting the
context to `never` when calling `yoga.handleNodeRequestAndResponse`; remove the
`as never` and instead define a proper server context type (e.g.
`ServerContext`) that matches what your Yoga instance expects, update the Yoga
creation to use that generic (e.g. `createYoga<ServerContext>(...)` or the
equivalent in your codebase), and pass an object typed as `ServerContext` into
`yoga.handleNodeRequestAndResponse` in the `app.route` handler so `app.route`,
`yoga.graphqlEndpoint`, and `yoga.handleNodeRequestAndResponse` all use the
correct context type and the compiler can validate it.

In `@apps/api/src/index.ts`:
- Around line 32-37: shutdownGracefully currently awaits closeAllQueueWorkers()
and app.close() without error handling, which can create unhandled rejections
when invoked from signal handlers; wrap the body of shutdownGracefully in a
try/catch/finally: in try await closeAllQueueWorkers() and await app.close(); in
catch log the error (include context and the caught error) using the existing
logger or console.error; in finally ensure process.exit(0) on success or
process.exit(1) on error so the process always exits. Reference
shutdownGracefully, closeAllQueueWorkers, and app.close when applying the
change.

In `@apps/api/src/lib/sentry.ts`:
- Line 52: Sentry.setupFastifyErrorHandler is a valid re-export but with
`@sentry/node` pinned to 9.3.0 you cannot use the shouldHandleError option (added
in 9.9.0) so Sentry will capture handled 4xx noise; either upgrade the
dependency to `@sentry/node` >= 9.9.0 and pass a shouldHandleError predicate to
Sentry.setupFastifyErrorHandler, or keep the current version and replace the
direct re-export by creating a thin wrapper that calls
Sentry.setupFastifyErrorHandler but also installs a Fastify setErrorHandler on
the server to filter out errors with statusCode < 500 before forwarding to
Sentry (reference Sentry.setupFastifyErrorHandler and the Fastify
setErrorHandler you will wrap).

Comment thread apps/api/src/app/billing/stripe.router.ts
Comment thread apps/api/src/app/github/middlewares/validate-webhook.middleware.ts Outdated
Comment thread apps/api/src/app/integrations/slack/middlewares/validate-webhook.middleware.ts Outdated
Comment thread apps/api/src/index.ts Outdated
Comment thread apps/api/src/lib/fastify-helpers.ts
Comment thread apps/api/src/bull-mq/bull-board.router.ts Fixed
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/app/billing/stripe.router.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/billing/stripe.router.ts`:
- Around line 39-40: The catch block passes the catch variable (typed as
unknown) directly to captureException, causing a TypeScript type error and
unsafe handling; update the catch handling in stripe.router.ts to normalize the
thrown value to an Error before calling captureException: check if error
instanceof Error and pass it directly, otherwise wrap it (e.g., new
Error(String(error))) or create a safe Error with contextual message, then call
captureException with that Error; ensure any subsequent code that reads
error.message or error.stack uses the normalized Error object.

In `@apps/api/src/bull-mq/bull-board.router.ts`:
- Around line 22-45: The auth hook is being added synchronously before the
rateLimit plugin loads so rate limiting never runs; fix it by registering the
rateLimit plugin first (await scope.register(rateLimit, ...)) and then
registering a nested child plugin that adds the auth hook (use
scope.register(async (inner) => { inner.addHook("onRequest", authHook) ... await
inner.register(serverAdapter.registerPlugin()) }, { prefix: env.BULLBOARD_PATH
})); reference the existing symbols auth, scope.addHook, rateLimit,
serverAdapter.registerPlugin, and env.BULLBOARD_PATH and ensure the auth hook
and serverAdapter registration occur inside the child scope so the rateLimit
hook executes before auth.

---

Duplicate comments:
In `@apps/api/src/app/billing/stripe.router.ts`:
- Line 72: The code uses a non-null assertion on Stripe.Checkout.Session.url
when calling reply.redirect(session.url!, 303), which could redirect to the
literal "null" if the SDK returns null; update the handler to guard the value:
check that session.url is a non-empty string before calling reply.redirect, and
if it's missing, log/record the error (using the existing logger) and return an
appropriate error response (e.g., reply.code(500).send or throw an HttpError)
instead of redirecting; reference session.url, reply.redirect, and
Stripe.Checkout.Session in your change.

In `@apps/api/src/lib/fastify-helpers.ts`:
- Around line 28-32: The current change ensures we only synthesize a stack when
the Error lacks one: retain the guard as written so logger.error("Fastify error
handler", error) runs, then if (!error.stack) Error.captureStackTrace(error) is
executed to preserve the original throw-site stack when present, and finally
call captureException(error); ensure these exact symbols (logger.error,
Error.captureStackTrace, captureException) remain in this order and the if
(!error.stack) guard is not removed or inverted.

Comment thread apps/api/src/app/billing/stripe.router.ts
Comment thread apps/api/src/bull-mq/bull-board.router.ts
@sweetr-dev sweetr-dev Bot added huge Huge PR - High risk of reviewer fatigue and removed large Large PR - Consider splitting up into smaller PRs to reduce risk and review time labels Feb 23, 2026
Comment thread apps/api/src/bull-mq/bull-board.router.ts Fixed
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 23, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/api/src/app/billing/stripe.router.ts
Comment thread apps/api/src/fastify.ts
Comment thread apps/api/src/app/billing/stripe.router.ts
Comment thread apps/api/src/bull-mq/bull-board.router.ts Outdated
Comment thread apps/api/src/fastify.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 23, 2026

Additional Comments (1)

apps/api/src/app/github/middlewares/validate-webhook.middleware.ts
Verify rawBody has the .update() method. If it's a string (due to encoding: "utf8"), this should work, but if it's sometimes a Buffer, ensure type consistency.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 23, 2026
Comment thread apps/api/src/bull-mq/bull-board.router.ts Fixed
@sweetr-dev sweetr-dev Bot added large Large PR - Consider splitting up into smaller PRs to reduce risk and review time and removed huge Huge PR - High risk of reviewer fatigue labels Feb 23, 2026
@waltergalvao waltergalvao merged commit bcde6a7 into main Feb 23, 2026
11 checks passed
@waltergalvao waltergalvao deleted the feat/fastify branch February 23, 2026 01:44
@coderabbitai coderabbitai Bot mentioned this pull request Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

large Large PR - Consider splitting up into smaller PRs to reduce risk and review time

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants