mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
- 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.
411 lines
15 KiB
Markdown
411 lines
15 KiB
Markdown
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<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 };
|
|
```
|
|
|
|
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. |