DEV Community

Cover image for Simple API CRUD done with ExpressoTS and Postgres
Richard Zampieri
Richard Zampieri

Posted on

Simple API CRUD done with ExpressoTS and Postgres

Choosing the right database connection strategy for your backend application often feels like navigating through a maze. There's no one-size-fits-all answer, as the best approach varies based on your application's needs. However, this post offers a strong, opinionated guide to help you think about what strategy to use.

When considering the database connection architecture, I break it down into two broad categories and then narrow down the choice based on specific application requirements.

High Performant Applications

These are applications that handle large data volumes, transactions, or numbers of users, focusing on low latency and high reliability. Note that "high-performance" can vary by domain and isn't just about speed. Features of High-Performance Applications include:

  • High Throughput
  • Data Intensive
  • Real Time
  • Low Latency
  • Event Driven
  • Stream Processing

Enterprise Applications

These are often large-scale applications built to serve specific business functions or industry requirements. Features include:

  • Integration capabilities
  • Extensible
  • User friendly
  • Data integrity
  • Compliance
  • Reporting and analytics

Exampled include ERP, CRM, SCM, CMS, Bi, E-commerce, etc.

Which Strategy is Right for You?

Here's a quick comparison table for Single Connection vs Connection Pooling:

(Pros) of Single Connection vs Connection Pool

Aspect Single Connection (Pros) Connection Pool (Pros)
Scalability Simpler to set up and manage. More scalable.
Performance No overhead for creating and destroying connections. Higher performance for concurrent requests.
Reliability Easier to debug since all queries go through one connection. Higher reliability.
Resource Utilization Lower resource utilization. Better resource utilization.
Complexity Less code complexity, easier to implement. Handles multiple connections gracefully.
Use Case Suitable for simple applications with limited concurrency. Suitable for read-heavy or write-heavy applications.

(Cons) of Single Connection vs Connection Pool

Aspect Single Connection (Cons) Connection Pool (Cons)
Scalability Limited scalability. Slightly more complex to manage.
Performance Lower performance for concurrent requests. Some overhead for managing the pool.
Reliability A single failure can impact all interactions. Possible connection leaks if not managed.
Resource Utilization Limited ability to utilize multiple CPU cores. Requires more memory and CPU.
Complexity May require application-level queueing. Increased code complexity.
Use Case Not suitable for read-heavy or write-heavy applications. May be overkill for very simple applications.

Use Case Recommendations

  • Connection Pooling: Best for read-heavy or write-heavy applications or those with many concurrent database operations.

  • Single Connection: Good enough for simple apps with limited concurrent database operations.

Most modern web apps benefit from connection pooling due to its scalability and performance advantages.

Sample Code with ExpressoTS and Postgres

Here's a quick code snippet to show how you might implement a connection pool using the ExpressoTS framework and Postgres' "pg" driver.

Provider responsible to create the pool

import { provideSingleton } from "@expressots/core";
import { Pool } from "pg";

/**
 * Configuration of the database pool.
 */
const pool = {
    host: "localhost",
    port: 5432,
    user: "postgres",
    password: "postgres",
    database: "expressots",
};

/**
 * Provider to inject the database pool into the container.
 */
@provideSingleton(PostgresProvider)
export class PostgresProvider {
    public get Pool(): Pool {
        return new Pool(pool);
    }
}
Enter fullscreen mode Exit fullscreen mode

Generic Repository pattern

@provide(BaseRepository)
export class BaseRepository<T extends IEntity> implements IBaseRepository<T> {
    protected db!: Pool;
    protected tableName: string;

    constructor() {
        this.db = container.get(PostgresProvider).Pool;
    }

    async create(item: T): Promise<T | null> {
        const client = await this.db.connect();

        try {
            const fields = Object.keys(item).join(", ");
            const values = Object.values(item);
            const placeholders = values
                .map((_, index) => `$${index + 1}`)
                .join(", ");
            const res: QueryResult = await client.query(
                `INSERT INTO ${this.tableName} (${fields}) VALUES (${placeholders}) RETURNING *`,
                values,
            );

            return res.rows[0] as T;
        } finally {
            client.release();
        }
    }

    async delete(id: string): Promise<boolean> {
        const client = await this.db.connect();

        try {
            const res: QueryResult = await client.query(
                `DELETE FROM ${this.tableName} WHERE id = $1`,
                [id],
            );
            return res.rowCount > 0;
        } finally {
            client.release();
        }
    }

    async update(item: T): Promise<T | null> {
        const client = await this.db.connect();

        try {
            const fields = Object.keys(item)
                .map((key, index) => `${key} = $${index + 1}`)
                .join(", ");
            const values = Object.values(item);
            const res: QueryResult = await client.query(
                `UPDATE ${this.tableName} SET ${fields} WHERE id = $${values.length} RETURNING *`,
                values,
            );
            return res.rows[0] as T;
        } finally {
            client.release();
        }
    }

    async find(id: string): Promise<T | null> {
        const client = await this.db.connect();

        try {
            const res: QueryResult = await client.query(
                `SELECT * FROM ${this.tableName} WHERE id = $1`,
                [id],
            );
            return res.rows[0] as T;
        } finally {
            client.release();
        }
    }

    async findAll(): Promise<T[]> {
        const client = await this.db.connect();
        console.log(this.tableName);
        try {
            const res: QueryResult = await client.query(
                `SELECT * FROM ${this.tableName}`,
            );
            return res.rows as T[];
        } finally {
            client.release();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A Create User use-case example

// Controller
@controller("/user/create")
export class UserCreateController extends BaseController {
    constructor(private createUserUseCase: CreateUserUseCase) {
        super();
    }

    @Post("/")
    async execute(
        @body() payload: CreateUserRequestDTO,
        @response() res: Response,
    ): Promise<CreateUserResponseDTO> {
        return this.callUseCase(
            await this.createUserUseCase.execute(payload),
            res,
            StatusCode.Created,
        );
    }
}

// Use Case
@provide(CreateUserUseCase)
export class CreateUserUseCase {
    constructor(
        private userRepository: UserRepository,
        private user: User,
        private report: Report,
    ) {}

    async execute(
        payload: CreateUserRequestDTO,
    ): Promise<CreateUserResponseDTO | AppError> {
        try {
            this.user.name = payload.name;
            this.user.email = payload.email;

            const userExists: User | null =
                await this.userRepository.findByEmail(this.user.email);

            if (userExists) {
                const error = this.report.error(
                    "User already exists",
                    StatusCode.BadRequest,
                    "create-user-usecase",
                );

                return error;
            }

            await this.userRepository.create(this.user);

            return {
                id: this.user.id,
                name: this.user.name,
                email: this.user.email,
                message: "user created successfully",
            };
        } catch (error: any) {
            throw error;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For more details, the complete code can be found here!

By considering these aspects, you'll be better prepared to choose the appropriate database connection strategy for your next project.

Top comments (0)