Okay, let's set up a skeleton Fastify API project with Prisma to interact with a PostgreSQL database. This structure will provide the requested endpoints. **Assumptions:** 1. You have Node.js and npm (or yarn) installed. 2. You have PostgreSQL installed and running locally. 3. You have a PostgreSQL database created (e.g., `finance_db`). 4. You have a PostgreSQL user with privileges on that database. **Project Setup Steps:** 1. **Create Project Directory & Initialize:** ```bash mkdir finance-api cd finance-api npm init -y ``` 2. **Install Dependencies:** ```bash # Runtime dependencies npm install fastify @prisma/client dotenv # Development dependencies npm install --save-dev prisma typescript @types/node ts-node nodemon # Or if you prefer plain Javascript, skip typescript/ts-node and adjust scripts ``` * `fastify`: The web framework. * `@prisma/client`: The Prisma database client. * `dotenv`: To load environment variables from a `.env` file. * `prisma`: The Prisma CLI (for migrations, generation). * `typescript`, `@types/node`, `ts-node`: For TypeScript support (recommended). * `nodemon`: To automatically restart the server during development. 3. **Initialize Prisma:** ```bash npx prisma init --datasource-provider postgresql ``` * This creates a `prisma` directory with a `schema.prisma` file and a `.env` file. 4. **Configure `.env`:** Edit the newly created `.env` file and set your PostgreSQL connection URL: ```dotenv # .env # Replace with your actual database user, password, host, port, and database name DATABASE_URL="postgresql://YOUR_USER:YOUR_PASSWORD@localhost:5432/finance_db?schema=public" # API Server Configuration (Optional, but good practice) API_HOST=0.0.0.0 API_PORT=3050 API_BASE_URL=https://finance.ptrwd.com # Used for documentation/reference, not binding ``` 5. **Define Prisma Schema (`prisma/schema.prisma`):** Define the model for your bank accounts. ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model BankAccount { id Int @id @default(autoincrement()) name String // e.g., "Checking Account", "Savings XYZ" bankName String // e.g., "Chase", "Wells Fargo" accountNumber String @unique // Consider encryption in a real app createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("bank_accounts") // Optional: specify table name in snake_case } ``` 6. **Create Initial Database Migration:** ```bash npx prisma migrate dev --name init_bank_account ``` * This command does two things: * Creates an SQL migration file in `prisma/migrations`. * Applies the migration to your database, creating the `bank_accounts` table. 7. **Generate Prisma Client:** Ensure the client is generated based on your schema: ```bash npx prisma generate ``` * This command reads your `schema.prisma` and generates the typed `@prisma/client`. You typically run this after any schema change. 8. **Create the Fastify Server (`src/server.ts` or `server.js`):** ```typescript // src/server.ts (if using TypeScript) // If using Javascript, remove type annotations and use require instead of import import Fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify'; import { Server, IncomingMessage, ServerResponse } from 'http'; import { PrismaClient, BankAccount } from '@prisma/client'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const prisma = new PrismaClient(); const server: FastifyInstance = Fastify({ logger: true // Enable logging }); // --- Type Definitions for Payloads/Params (Good Practice) --- interface BankAccountCreateParams { name: string; bankName: string; accountNumber: string; } interface BankAccountUpdateParams { id: string; } interface BankAccountUpdateBody { name?: string; bankName?: string; accountNumber?: string; } interface BankAccountDeleteParams { id: string; } // --- API Routes --- const API_PREFIX = '/api/bank-account'; // 1. Create Bank Account server.post<{ Body: BankAccountCreateParams }>( `${API_PREFIX}/create`, async (request, reply): Promise => { try { const { name, bankName, accountNumber } = request.body; const newAccount = await prisma.bankAccount.create({ data: { name, bankName, accountNumber, }, }); reply.code(201); // Resource Created return newAccount; } catch (error: any) { server.log.error(error); // Basic duplicate check example (Prisma throws P2002) if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { reply.code(409); // Conflict throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`); } reply.code(500); throw new Error('Failed to create bank account.'); } } ); // 2. Update Bank Account server.post<{ Params: BankAccountUpdateParams; Body: BankAccountUpdateBody }>( `${API_PREFIX}/update/:id`, async (request, reply): Promise => { try { const { id } = request.params; const updateData = request.body; // Ensure ID is a valid number before querying const accountId = parseInt(id, 10); if (isNaN(accountId)) { reply.code(400); throw new Error('Invalid account ID format.'); } const updatedAccount = await prisma.bankAccount.update({ where: { id: accountId }, data: updateData, }); return updatedAccount; } catch (error: any) { server.log.error(error); // Handle case where account doesn't exist (Prisma throws P2025) if (error.code === 'P2025') { reply.code(404); // Not Found throw new Error(`Bank account with ID ${request.params.id} not found.`); } // Handle potential duplicate account number on update if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { reply.code(409); // Conflict throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`); } reply.code(500); throw new Error('Failed to update bank account.'); } } ); // 3. Delete Bank Account server.delete<{ Params: BankAccountDeleteParams }>( `${API_PREFIX}/delete/:id`, async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => { try { const { id } = request.params; // Ensure ID is a valid number const accountId = parseInt(id, 10); if (isNaN(accountId)) { reply.code(400); throw new Error('Invalid account ID format.'); } const deletedAccount = await prisma.bankAccount.delete({ where: { id: accountId }, }); return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount }; } catch (error: any) { server.log.error(error); // Handle case where account doesn't exist (Prisma throws P2025) if (error.code === 'P2025') { reply.code(404); // Not Found throw new Error(`Bank account with ID ${request.params.id} not found.`); } reply.code(500); throw new Error('Failed to delete bank account.'); } } ); // 4. Get All Bank Accounts server.get( `${API_PREFIX}/`, // Using trailing slash for consistency, Fastify often handles both async (request, reply): Promise => { try { const accounts = await prisma.bankAccount.findMany({ orderBy: { // Optional: Order results createdAt: 'desc' } }); return accounts; } catch (error: any) { server.log.error(error); reply.code(500); throw new Error('Failed to retrieve bank accounts.'); } } ); // Optional: Get Single Bank Account by ID (Often useful) server.get<{ Params: { id: string } }>( `${API_PREFIX}/:id`, async (request, reply): Promise => { try { const { id } = request.params; const accountId = parseInt(id, 10); if (isNaN(accountId)) { reply.code(400); throw new Error('Invalid account ID format.'); } const account = await prisma.bankAccount.findUnique({ where: { id: accountId }, }); if (!account) { reply.code(404); throw new Error(`Bank account with ID ${id} not found.`); } return account; } catch (error: any) { // Catch errors that might have been thrown before prisma query if (!reply.sent) { // Check if a reply hasn't already been sent (e.g., by 400 error) server.log.error(error); reply.code(500); throw new Error('Failed to retrieve bank account.'); } // If reply already sent, just rethrow the original error throw error; } } ); // --- Graceful Shutdown --- const gracefulShutdown = async (signal: string) => { console.log(`*^! Received signal ${signal}. Shutting down...`); try { await server.close(); console.log('Fastify server closed.'); await prisma.$disconnect(); console.log('Prisma client disconnected.'); process.exit(0); } catch (err) { console.error('Error during shutdown:', err); process.exit(1); } }; process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // --- Start Server --- const start = async () => { try { const host = process.env.API_HOST || '0.0.0.0'; const port = parseInt(process.env.API_PORT || '3050', 10); await server.listen({ port, host }); // Log after listen resolves (Fastify v3+) // server.log.info(`Server listening at http://${host}:${port}`); // Note: Fastify logger automatically logs listening address } catch (err) { server.log.error(err); await prisma.$disconnect(); // Ensure prisma disconnects on startup error too process.exit(1); } }; start(); // Export prisma instance if needed elsewhere (e.g., tests, scripts) // export { prisma }; ``` 9. **Configure `tsconfig.json` (if using TypeScript):** Create a `tsconfig.json` file in the root directory: ```json // tsconfig.json { "compilerOptions": { "target": "ES2017", // Or newer "module": "CommonJS", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "sourceMap": true, // Helpful for debugging "resolveJsonModule": true // Allows importing JSON files }, "include": ["src/**/*"], // Only compile files in src "exclude": ["node_modules", "**/*.spec.ts"] // Exclude tests if you have them } ``` 10. **Add Scripts to `package.json`:** ```json // package.json (add or modify the scripts section) "scripts": { "build": "tsc", // Compile TypeScript to JavaScript (if using TS) "start": "node dist/server.js", // Run compiled JS (if using TS) "dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts", // Dev mode with TS // --- OR if using plain JavaScript --- // "start": "node server.js", // "dev": "nodemon server.js", // --- Prisma commands for convenience --- "prisma:migrate": "prisma migrate dev", "prisma:generate": "prisma generate", "prisma:studio": "prisma studio" // GUI to view/edit data }, ``` 11. **Run the Development Server:** ```bash npm run dev ``` **How to Test (using `curl`):** *(Replace `YOUR_ACCOUNT_ID` with an actual ID after creating an account)* * **Create:** ```bash curl -X POST -H "Content-Type: application/json" \ -d '{"name": "My Checking", "bankName": "Local Bank", "accountNumber": "123456789"}' \ http://localhost:3050/api/bank-account/create ``` * **Get All:** ```bash curl http://localhost:3050/api/bank-account/ ``` * **Update (using the ID returned from create):** ```bash curl -X POST -H "Content-Type: application/json" \ -d '{"name": "My Primary Checking"}' \ http://localhost:3050/api/bank-account/update/YOUR_ACCOUNT_ID ``` * **Delete (using the ID):** ```bash curl -X DELETE http://localhost:3050/api/bank-account/delete/YOUR_ACCOUNT_ID ``` * **Get Specific (using the ID):** ```bash curl http://localhost:3050/api/bank-account/YOUR_ACCOUNT_ID ``` This skeleton provides the core structure. You can build upon this by adding more robust error handling, input validation (using Fastify's built-in schema validation), authentication/authorization, more complex queries, and organizing routes into separate files/plugins as the application grows.