mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
feat: initialize finance project with Fastify, Prisma, and Zod
- 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.
This commit is contained in:
411
overview.md
Normal file
411
overview.md
Normal file
@@ -0,0 +1,411 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user