Files
finance/overview.md
Peter Wood d150757025 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.
2025-04-05 17:52:40 -04:00

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:

  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:

    mkdir finance-api
    cd finance-api
    npm init -y
    
  2. 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 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:

    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:

    # .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/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:

    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:

    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):

    // 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:

    // 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:

    // 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:

    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.