The Backend Bible - Fastify, TypeScript & MongoDB

No description provided.

Install via CLI
$ npx @airuleshub/cli@latest add the-backend-bible-fastify-typescript-mongodb

Rule Content

Fastify + TypeScript + MongoDB Backend - Project Initialization Guide

AI Instruction: Use this document to fully scaffold and initialize the backend project. Follow each section in order. Do not skip steps. Generate all files with the exact structure and content described.


Tech Stack

LayerTechnology
RuntimeNode.js (v20+)
FrameworkFastify v4
LanguageTypeScript (strict mode)
DatabaseMongoDB (via Mongoose v8)
ValidationZod
Environmentdotenv
Dev Toolingtsx, ts-node, eslint, prettier

Folder Structure

Generate the following folder and file structure exactly:

project-root/├── src/│   ├── config/│   │   ├── db.ts                  # MongoDB connection logic│   │   └── env.ts                 # Zod-validated environment variables│   ├── modules/│   │   └── example/│   │       ├── example.controller.ts│   │       ├── example.route.ts│   │       ├── example.schema.ts  # Zod schemas│   │       ├── example.model.ts   # Mongoose model│   │       └── example.service.ts│   ├── plugins/│   │   ├── sensible.ts            # fastify-sensible plugin│   │   └── swagger.ts             # Swagger/OpenAPI plugin (optional)│   ├── hooks/│   │   └── auth.hook.ts           # Global auth hooks (if needed)│   ├── utils/│   │   ├── logger.ts              # Custom logger wrapper│   │   └── response.ts            # Standard API response helpers│   ├── types/│   │   └── index.d.ts             # Global type augmentations│   ├── app.ts                     # Fastify app factory│   └── server.ts                  # Entry point — starts server├── .env├── .env.example├── .eslintrc.json├── .prettierrc├── tsconfig.json├── package.json└── README.md

Step 1 — Initialize the Project

Run the following commands:

bash
mkdir project-root && cd project-rootnpm init -ygit init

Step 2 — Install Dependencies

bash
# Production dependenciesnpm install fastify @fastify/sensible @fastify/cors mongoose zod dotenv
# Development dependenciesnpm install -D typescript tsx ts-node @types/node \  eslint prettier eslint-config-prettier \  @typescript-eslint/parser @typescript-eslint/eslint-plugin

Step 3 — TypeScript Configuration

File: tsconfig.json

json
{  "compilerOptions": {    "target": "ES2022",    "module": "NodeNext",    "moduleResolution": "NodeNext",    "lib": ["ES2022"],    "outDir": "./dist",    "rootDir": "./src",    "strict": true,    "esModuleInterop": true,    "skipLibCheck": true,    "forceConsistentCasingInFileNames": true,    "resolveJsonModule": true,    "declaration": true,    "declarationMap": true,    "sourceMap": true  },  "include": ["src/**/*"],  "exclude": ["node_modules", "dist"]}

Step 4 — Package.json Scripts

Add the following scripts block to package.json:

json
{  "type": "module",  "scripts": {    "dev": "tsx watch src/server.ts",    "build": "tsc",    "start": "node dist/server.js",    "lint": "eslint src --ext .ts",    "format": "prettier --write src/**/*.ts"  }}

Step 5 — Environment Variables

File: .env.example

env
NODE_ENV=developmentPORT=3000HOST=0.0.0.0MONGODB_URI=mongodb://localhost:27017/mydbJWT_SECRET=your_jwt_secret_here

File: .env — copy from .env.example and fill in real values.


Step 6 — Zod Environment Validation

File: src/config/env.ts

typescript
import { z } from 'zod';import dotenv from 'dotenv';
dotenv.config();
const envSchema = z.object({  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),  PORT: z.coerce.number().default(3000),  HOST: z.string().default('0.0.0.0'),  MONGODB_URI: z.string().url(),  JWT_SECRET: z.string().min(16),});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {  console.error('❌ Invalid environment variables:');  console.error(parsed.error.flatten().fieldErrors);  process.exit(1);}
export const env = parsed.data;

Step 7 — MongoDB Connection

File: src/config/db.ts

typescript
import mongoose from 'mongoose';import { env } from './env.js';
export async function connectDB(): Promise<void> {  try {    await mongoose.connect(env.MONGODB_URI);    console.log('✅ MongoDB connected');  } catch (error) {    console.error('❌ MongoDB connection error:', error);    process.exit(1);  }}
export async function disconnectDB(): Promise<void> {  await mongoose.disconnect();  console.log('🔌 MongoDB disconnected');}

Step 8 — Fastify App Factory

File: src/app.ts

typescript
import Fastify, { FastifyInstance } from 'fastify';import sensible from '@fastify/sensible';import cors from '@fastify/cors';
import { exampleRoutes } from './modules/example/example.route.js';
export async function buildApp(): Promise<FastifyInstance> {  const app = Fastify({    logger: {      level: 'info',      transport: {        target: 'pino-pretty',        options: { colorize: true },      },    },  });
  // Plugins  await app.register(sensible);  await app.register(cors, { origin: true });
  // Routes  await app.register(exampleRoutes, { prefix: '/api/v1/examples' });
  // Health check  app.get('/health', async () => ({ status: 'ok' }));
  return app;}

Step 9 — Server Entry Point

File: src/server.ts

typescript
import { buildApp } from './app.js';import { connectDB, disconnectDB } from './config/db.js';import { env } from './config/env.js';
async function main() {  const app = await buildApp();
  await connectDB();
  try {    await app.listen({ port: env.PORT, host: env.HOST });    console.log(`🚀 Server running at http://${env.HOST}:${env.PORT}`);  } catch (err) {    app.log.error(err);    await disconnectDB();    process.exit(1);  }
  // Graceful shutdown  const shutdown = async (signal: string) => {    console.log(`\n⚠️  Received ${signal}. Shutting down...`);    await app.close();    await disconnectDB();    process.exit(0);  };
  process.on('SIGINT', () => shutdown('SIGINT'));  process.on('SIGTERM', () => shutdown('SIGTERM'));}
main();

Step 10 — Example Module

Zod Schema

File: src/modules/example/example.schema.ts

typescript
import { z } from 'zod';
export const createExampleSchema = z.object({  name: z.string().min(1, 'Name is required').max(100),  description: z.string().optional(),  isActive: z.boolean().default(true),});
export const updateExampleSchema = createExampleSchema.partial();
export const exampleParamsSchema = z.object({  id: z.string().length(24, 'Invalid MongoDB ObjectId'),});
export type CreateExampleInput = z.infer<typeof createExampleSchema>;export type UpdateExampleInput = z.infer<typeof updateExampleSchema>;export type ExampleParams = z.infer<typeof exampleParamsSchema>;

Mongoose Model

File: src/modules/example/example.model.ts

typescript
import mongoose, { Document, Schema } from 'mongoose';
export interface IExample extends Document {  name: string;  description?: string;  isActive: boolean;  createdAt: Date;  updatedAt: Date;}
const exampleSchema = new Schema<IExample>(  {    name: { type: String, required: true, trim: true },    description: { type: String },    isActive: { type: Boolean, default: true },  },  {    timestamps: true,    versionKey: false,  });
export const Example = mongoose.model<IExample>('Example', exampleSchema);

Service Layer

File: src/modules/example/example.service.ts

typescript
import { Example, IExample } from './example.model.js';import { CreateExampleInput, UpdateExampleInput } from './example.schema.js';
export async function getAllExamples(): Promise<IExample[]> {  return Example.find({ isActive: true }).lean();}
export async function getExampleById(id: string): Promise<IExample | null> {  return Example.findById(id).lean();}
export async function createExample(data: CreateExampleInput): Promise<IExample> {  const example = new Example(data);  return example.save();}
export async function updateExample(  id: string,  data: UpdateExampleInput): Promise<IExample | null> {  return Example.findByIdAndUpdate(id, data, { new: true, runValidators: true }).lean();}
export async function deleteExample(id: string): Promise<IExample | null> {  return Example.findByIdAndDelete(id).lean();}

Controller

File: src/modules/example/example.controller.ts

typescript
import { FastifyRequest, FastifyReply } from 'fastify';import { CreateExampleInput, ExampleParams, UpdateExampleInput } from './example.schema.js';import * as service from './example.service.js';
export async function getAll(req: FastifyRequest, reply: FastifyReply) {  const data = await service.getAllExamples();  return reply.send({ success: true, data });}
export async function getOne(  req: FastifyRequest<{ Params: ExampleParams }>,  reply: FastifyReply) {  const item = await service.getExampleById(req.params.id);  if (!item) return reply.notFound('Example not found');  return reply.send({ success: true, data: item });}
export async function create(  req: FastifyRequest<{ Body: CreateExampleInput }>,  reply: FastifyReply) {  const data = await service.createExample(req.body);  return reply.status(201).send({ success: true, data });}
export async function update(  req: FastifyRequest<{ Params: ExampleParams; Body: UpdateExampleInput }>,  reply: FastifyReply) {  const data = await service.updateExample(req.params.id, req.body);  if (!data) return reply.notFound('Example not found');  return reply.send({ success: true, data });}
export async function remove(  req: FastifyRequest<{ Params: ExampleParams }>,  reply: FastifyReply) {  const data = await service.deleteExample(req.params.id);  if (!data) return reply.notFound('Example not found');  return reply.send({ success: true, message: 'Deleted successfully' });}

Route Registration

File: src/modules/example/example.route.ts

typescript
import { FastifyInstance } from 'fastify';import { zodToJsonSchema } from 'zod-to-json-schema';import {  createExampleSchema,  updateExampleSchema,  exampleParamsSchema,} from './example.schema.js';import * as controller from './example.controller.js';
export async function exampleRoutes(app: FastifyInstance) {  app.get('/', { schema: { tags: ['Examples'] } }, controller.getAll);
  app.get(    '/:id',    { schema: { tags: ['Examples'], params: zodToJsonSchema(exampleParamsSchema) } },    controller.getOne  );
  app.post(    '/',    {      schema: {        tags: ['Examples'],        body: zodToJsonSchema(createExampleSchema),      },    },    controller.create  );
  app.put(    '/:id',    {      schema: {        tags: ['Examples'],        params: zodToJsonSchema(exampleParamsSchema),        body: zodToJsonSchema(updateExampleSchema),      },    },    controller.update  );
  app.delete(    '/:id',    { schema: { tags: ['Examples'], params: zodToJsonSchema(exampleParamsSchema) } },    controller.remove  );}

Note: Install zod-to-json-schema with: npm install zod-to-json-schema


Step 11 — Utility Helpers

File: src/utils/response.ts

typescript
export function successResponse<T>(data: T, message = 'Success') {  return { success: true, message, data };}
export function errorResponse(message: string, errors?: unknown) {  return { success: false, message, errors };}

Step 12 — ESLint & Prettier Config

File: .eslintrc.json

json
{  "parser": "@typescript-eslint/parser",  "plugins": ["@typescript-eslint"],  "extends": [    "eslint:recommended",    "plugin:@typescript-eslint/recommended",    "prettier"  ],  "env": { "node": true, "es2022": true },  "rules": {    "@typescript-eslint/no-unused-vars": ["warn"],    "@typescript-eslint/explicit-function-return-type": "off"  }}

File: .prettierrc

json
{  "semi": true,  "singleQuote": true,  "trailingComma": "es5",  "printWidth": 100,  "tabWidth": 2}

Step 13 — .gitignore

File: .gitignore

node_modules/dist/.env*.log.DS_Store

Conventions & Patterns

Adding a New Module

To add a new module (e.g., user), create the following files following the same pattern:

src/modules/user/├── user.controller.ts├── user.route.ts├── user.schema.ts├── user.model.ts└── user.service.ts

Then register the route in src/app.ts:

typescript
await app.register(userRoutes, { prefix: '/api/v1/users' });

Validation Pattern

Always validate input using Zod schemas in the schema file. Pass type-safe inferred types to controllers and services. Never bypass Zod for request body or params.

Error Handling

Use reply.notFound(), reply.badRequest(), and reply.internalServerError() from @fastify/sensible. Wrap async service calls in try/catch at the controller level for unexpected errors.


API Endpoints (Example Module)

MethodPathDescription
GET/healthHealth check
GET/api/v1/examplesList all examples
GET/api/v1/examples/:idGet one by ID
POST/api/v1/examplesCreate new
PUT/api/v1/examples/:idUpdate by ID
DELETE/api/v1/examples/:idDelete by ID

Final Checklist for AI Initialization

  • Run npm install after generating package.json
  • Create .env from .env.example with real values
  • Ensure MongoDB is running locally or provide Atlas URI
  • Run npm run dev to verify the server starts
  • Hit GET /health to confirm the app is live
  • Check MongoDB connection log for ✅ MongoDB connected

Command Palette

Search for a command to run...