- Added package.json with necessary dependencies and scripts for development and production. - Created Prisma schema for BankAccount model with fields: id, name, bankName, accountNumber, createdAt, and updatedAt. - Implemented Fastify server with CRUD operations for bank accounts, including validation using Zod. - Added graceful shutdown handling for server and Prisma client.
15 KiB
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:
- You have Node.js and npm (or yarn) installed.
- You have PostgreSQL installed and running locally.
- You have a PostgreSQL database created (e.g.,
finance_db). - You have a PostgreSQL user with privileges on that database.
Project Setup Steps:
-
Create Project Directory & Initialize:
mkdir finance-api cd finance-api npm init -y -
Install Dependencies:
# 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 scriptsfastify: The web framework.@prisma/client: The Prisma database client.dotenv: To load environment variables from a.envfile.prisma: The Prisma CLI (for migrations, generation).typescript,@types/node,ts-node: For TypeScript support (recommended).nodemon: To automatically restart the server during development.
-
Initialize Prisma:
npx prisma init --datasource-provider postgresql- This creates a
prismadirectory with aschema.prismafile and a.envfile.
- This creates a
-
Configure
.env: Edit the newly created.envfile and set your PostgreSQL connection URL:# .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 -
Define Prisma Schema (
prisma/schema.prisma): Define the model for your bank accounts.// 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 } -
Create Initial Database Migration:
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_accountstable.
- Creates an SQL migration file in
- This command does two things:
-
Generate Prisma Client: Ensure the client is generated based on your schema:
npx prisma generate- This command reads your
schema.prismaand generates the typed@prisma/client. You typically run this after any schema change.
- This command reads your
-
Create the Fastify Server (
src/server.tsorserver.js):// 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<BankAccount> => { 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<BankAccount> => { 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<BankAccount[]> => { 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<BankAccount> => { 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 }; -
Configure
tsconfig.json(if using TypeScript): Create atsconfig.jsonfile in the root directory:// 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 } -
Add Scripts to
package.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 }, -
Run the Development Server:
npm run dev
How to Test (using curl):
(Replace YOUR_ACCOUNT_ID with an actual ID after creating an account)
-
Create:
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:
curl http://localhost:3050/api/bank-account/ -
Update (using the ID returned from create):
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):
curl -X DELETE http://localhost:3050/api/bank-account/delete/YOUR_ACCOUNT_ID -
Get Specific (using the ID):
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.