diff --git a/.changeset/light-cats-find.md b/.changeset/light-cats-find.md new file mode 100644 index 0000000..af3bcc2 --- /dev/null +++ b/.changeset/light-cats-find.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add fastify diff --git a/.gitignore b/.gitignore index b2201cc..344eb8b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ yarn-error.log* # Misc .DS_Store *.pem +.vscode diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 2ae1357..411b9dc 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -74,6 +74,9 @@ export const dependencyVersionMap = { "@types/express": "^5.0.1", "@types/cors": "^2.8.17", + fastify: "^5.3.3", + "@fastify/cors": "^11.0.1", + turbo: "^2.4.2", ai: "^4.3.16", diff --git a/apps/cli/src/helpers/backend-framework-setup.ts b/apps/cli/src/helpers/backend-framework-setup.ts index da8e470..a4a51af 100644 --- a/apps/cli/src/helpers/backend-framework-setup.ts +++ b/apps/cli/src/helpers/backend-framework-setup.ts @@ -42,6 +42,12 @@ export async function setupBackendDependencies( dependencies.push("express", "cors"); devDependencies.push("@types/express", "@types/cors"); + if (runtime === "node") { + devDependencies.push("tsx", "@types/node"); + } + } else if (framework === "fastify") { + dependencies.push("fastify", "@fastify/cors"); + if (runtime === "node") { devDependencies.push("tsx", "@types/node"); } diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index fbcce33..18726cd 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -268,6 +268,8 @@ function generateFeaturesList( addonsList.push("- **Hono** - Lightweight, performant server framework"); } else if (backend === "express") { addonsList.push("- **Express** - Fast, unopinionated web framework"); + } else if (backend === "fastify") { + addonsList.push("- **Fastify** - Fast, low-overhead web framework"); } else if (backend === "elysia") { addonsList.push("- **Elysia** - Type-safe, high-performance framework"); } else if (backend === "next") { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index b719eeb..5d67653 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -140,7 +140,15 @@ async function main() { .option("backend", { type: "string", describe: "Backend framework", - choices: ["hono", "express", "next", "elysia", "convex", "none"], + choices: [ + "hono", + "express", + "fastify", + "next", + "elysia", + "convex", + "none", + ], }) .option("runtime", { type: "string", diff --git a/apps/cli/src/prompts/backend-framework.ts b/apps/cli/src/prompts/backend-framework.ts index 355948a..c6eba28 100644 --- a/apps/cli/src/prompts/backend-framework.ts +++ b/apps/cli/src/prompts/backend-framework.ts @@ -33,6 +33,11 @@ export async function getBackendFrameworkChoice( label: "Express", hint: "Fast, unopinionated, minimalist web framework for Node.js", }, + { + value: "fastify" as const, + label: "Fastify", + hint: "Fast, low-overhead web framework for Node.js", + }, { value: "elysia" as const, label: "Elysia", diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index f539c1a..574468d 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -17,6 +17,7 @@ export type ProjectAddons = export type ProjectBackend = | "hono" | "express" + | "fastify" | "next" | "elysia" | "convex" diff --git a/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs b/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs index 3c651c2..d14560c 100644 --- a/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs +++ b/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs @@ -91,6 +91,29 @@ export async function createContext(opts: any) { {{/if}} } +{{else if (eq backend 'fastify')}} +import type { IncomingHttpHeaders } from "node:http"; +{{#if auth}} +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "./auth"; +{{/if}} + +export async function createContext(req: IncomingHttpHeaders) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: fromNodeHeaders(req), + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + {{else}} export async function createContext() { return { diff --git a/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs b/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs index 8ba9e4d..927c893 100644 --- a/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs +++ b/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs @@ -95,6 +95,27 @@ export async function createContext(opts: CreateExpressContextOptions) { {{/if}} } +{{else if (eq backend 'fastify')}} +import type { CreateFastifyContextOptions } from "@trpc/server/adapters/fastify"; +{{#if auth}} +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "./auth"; +{{/if}} + +export async function createContext({ req, res }: CreateFastifyContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: fromNodeHeaders(req.headers), + }); + return { session }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + {{else}} export async function createContext() { return { diff --git a/apps/cli/templates/backend/server/fastify/src/index.ts.hbs b/apps/cli/templates/backend/server/fastify/src/index.ts.hbs new file mode 100644 index 0000000..6b0d2f7 --- /dev/null +++ b/apps/cli/templates/backend/server/fastify/src/index.ts.hbs @@ -0,0 +1,155 @@ +import "dotenv/config"; +import Fastify from "fastify"; +import fastifyCors from "@fastify/cors"; + +{{#if (eq api "trpc")}} +import { fastifyTRPCPlugin, type FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify"; +import { createContext } from "./lib/context"; +import { appRouter, type AppRouter } from "./routers/index"; +{{/if}} + +{{#if (eq api "orpc")}} +import { RPCHandler } from "@orpc/server/node"; +import { CORSPlugin } from "@orpc/server/plugins"; +import { appRouter } from "./routers/index"; +import { createServer } from "node:http"; +{{#if auth}} +import { createContext } from "./lib/context"; +{{/if}} +{{/if}} + +{{#if (includes examples "ai")}} +import type { FastifyRequest, FastifyReply } from "fastify"; +import { streamText, type Message } from "ai"; +import { google } from "@ai-sdk/google"; +{{/if}} + +{{#if auth}} +import { auth } from "./lib/auth"; +{{/if}} + +const baseCorsConfig = { + origin: process.env.CORS_ORIGIN || "", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "X-Requested-With" + ], + credentials: true, + maxAge: 86400, +}; + +{{#if (eq api "orpc")}} +const handler = new RPCHandler(appRouter, { + plugins: [ + new CORSPlugin({ + origin: process.env.CORS_ORIGIN, + credentials: true, + allowHeaders: ["Content-Type", "Authorization"], + }), + ], +}); + +const fastify = Fastify({ + logger: true, + serverFactory: (fastifyHandler) => { + const server = createServer(async (req, res) => { + const { matched } = await handler.handle(req, res, { + context: await createContext(req.headers), + prefix: "/rpc", + }); + + if (matched) { + return; + } + + fastifyHandler(req, res); + }); + + return server; + }, +}); +{{else}} +const fastify = Fastify({ + logger: true, +}); +{{/if}} + +fastify.register(fastifyCors, baseCorsConfig); + +{{#if auth}} +fastify.route({ + method: ["GET", "POST"], + url: "/api/auth/*", + async handler(request, reply) { + try { + const url = new URL(request.url, `http://${request.headers.host}`); + const headers = new Headers(); + Object.entries(request.headers).forEach(([key, value]) => { + if (value) headers.append(key, value.toString()); + }); + const req = new Request(url.toString(), { + method: request.method, + headers, + body: request.body ? JSON.stringify(request.body) : undefined, + }); + const response = await auth.handler(req); + reply.status(response.status); + response.headers.forEach((value, key) => reply.header(key, value)); + reply.send(response.body ? await response.text() : null); + } catch (error) { + fastify.log.error("Authentication Error:", error); + reply.status(500).send({ + error: "Internal authentication error", + code: "AUTH_FAILURE" + }); + } + } +}); +{{/if}} + +{{#if (eq api "trpc")}} +fastify.register(fastifyTRPCPlugin, { + prefix: "/trpc", + trpcOptions: { + router: appRouter, + createContext, + onError({ path, error }) { + console.error(`Error in tRPC handler on path '${path}':`, error); + }, + } satisfies FastifyTRPCPluginOptions["trpcOptions"], +}); +{{/if}} + +{{#if (includes examples "ai")}} +interface AiRequestBody { + id?: string; + messages: Message[]; +} + +fastify.post('/ai', async function (request, reply) { + const { messages } = request.body as AiRequestBody; + const result = streamText({ + model: google('gemini-1.5-flash'), + messages, + }); + + reply.header('X-Vercel-AI-Data-Stream', 'v1'); + reply.header('Content-Type', 'text/plain; charset=utf-8'); + + return reply.send(result.toDataStream()); +}); +{{/if}} + +fastify.get('/', async () => { + return 'OK' +}) + +fastify.listen({ port: 3000 }, (err) => { + if (err) { + fastify.log.error(err); + process.exit(1); + } + console.log("Server running on port 3000"); +}); diff --git a/apps/web/components/features.tsx b/apps/web/components/features.tsx index 3712273..69acd42 100644 --- a/apps/web/components/features.tsx +++ b/apps/web/components/features.tsx @@ -12,7 +12,7 @@ export default function Features() { } title="Flexible Backend" - description="Choose between Hono, Elysia, Next.js and Express" + description="Choose between Hono, Elysia, Next.js, Express, and Fastify" /> } diff --git a/apps/web/public/icon/fastify.svg b/apps/web/public/icon/fastify.svg new file mode 100644 index 0000000..51728e5 --- /dev/null +++ b/apps/web/public/icon/fastify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 6a428d7..e66befe 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -163,6 +163,13 @@ export const TECH_OPTIONS = { icon: "/icon/express.svg", color: "from-gray-500 to-gray-700", }, + { + id: "fastify", + name: "Fastify", + description: "Fast, low-overhead web framework for Node.js", + icon: "/icon/fastify.svg", + color: "from-gray-500 to-gray-700", + }, { id: "convex", name: "Convex",