ChatGPT Image Aug 11, 2025, 03_57_57 PM

A Fast, Opinionated Node + Express + TypeScript Boilerplate (with real-world error handling)

I built a lightweight but opinionated Express + TypeScript starter that lets me spin up APIs in minutes without re-doing the boring parts. It ships with strict typing, a clean folder layout, a predictable error model, production-ready middleware, and batteries-included testing so you can focus on your domain logic.

GitHub: https://github.com/kalemmentore868/boilerplate-node-ts-api


Why another boilerplate?

Most starters either feel too bare (you still copy/paste half your stack) or too heavy (framework on top of framework). This one aims for the sweet spot:

  • TypeScript-first with strict types and path aliases
  • Predictable errors from day one (details below)
  • Sane security & DX middleware (CORS, Helmet, rate limiting, logging)
  • Testing that mirrors production (request-level tests with Supertest)

The error model: explicit, typed, and boring-on-purpose

Errors should be boring. The boilerplate uses a tiny class hierarchy so every failure you throw maps to a clear HTTP response:

class ExpressError extends Error {
  status;
  constructor(message: string, status: number) {
    super();
    this.message = message;
    this.status = status;
  }
}

class NotFoundError extends ExpressError {
  constructor(message = "Not Found") { super(message, 404); }
}
class UnauthorizedError extends ExpressError {
  constructor(message = "Unauthorized") { super(message, 401); }
}
class BadRequestError extends ExpressError {
  constructor(message = "Bad Request") { super(message, 400); }
}
class ForbiddenError extends ExpressError {
  constructor(message = "Forbidden Request") { super(message, 403); }
}
class InternalServerError extends ExpressError {
  constructor(message = "Internal Server Error") { super(message, 500); }
}

What’s unique here?

  1. One source of truth for status codes.
    You throw domain-friendly classes (new BadRequestError("Email required")) and never repeat the status in handlers.
  2. Consistent JSON shape.
    The global error middleware serializes every error to the same response structure—great for clients and logging.
  3. Type-narrowing friendly.
    Since everything extends ExpressError, the handler can confidently read err.status without guessing.

Global error handler:

import type { ErrorRequestHandler } from "express";
import { ExpressError, InternalServerError } from "./errors";

export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
  if (err instanceof ExpressError) {
    return res.status(err.status).json({
      error: { message: err.message, status: err.status },
    });
  }
  const fallback = new InternalServerError();
  return res.status(fallback.status).json({
    error: { message: fallback.message, status: fallback.status },
  });
};

Use in routes:

app.get("/api/users/:id", async (req, res) => {
  const user = await repo.find(req.params.id);
  if (!user) throw new NotFoundError("User not found");
  res.json({ data: user });
});

Result: fewer if/else branches, zero duplicate status codes, cleaner controllers.


Middleware: opinionated defaults you can keep or swap

  • Security: helmet() by default, sensible CORS configuration, and express-rate-limit to dampen abuse.
  • Request tracing: a tiny requestId middleware + morgan log format that includes the ID so logs and client issues correlate.
  • Validation helper (Zod friendly): a validate(schema) middleware you can drop into routes.

Example validator:

import { z } from "zod";
import type { RequestHandler } from "express";

export const validate =
  (schema: z.ZodSchema): RequestHandler =>
  (req, _res, next) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });
    if (!result.success)
      return next(new BadRequestError(result.error.issues[0]?.message ?? "Invalid request"));
    next();
  };

Testing setup: API-first, not unit-only

I prefer to test APIs the way they’re used in the wild—through HTTP. The boilerplate includes:

  • Jest + Supertest for request-level tests
  • TypeScript support via a transformer (e.g., ts-jest/swc-jest)
  • Isolated env: .env.test loaded for tests so you can point to a test DB or in-memory store
  • Lightweight factories (optional) for creating test data

Example test:

import request from "supertest";
import { app } from "../app";

describe("Auth", () => {
  it("returns 401 on bad credentials", async () => {
    const res = await request(app)
      .post("/api/auth/login")
      .send({ email: "x@y.z", password: "nope" });
    expect(res.status).toBe(401);
    expect(res.body.error.message).toMatch(/unauthorized/i);
  });
});

Because the error shape is consistent, assertions are trivial and stable over time.


Folder structure (brief)

src/
  app.ts            # Express app
  server.ts         # boot (separate from app to aid testing)
  errors/           # error classes
  middleware/       # errorHandler, requestId, validate, rateLimit, etc.
  routes/           # modular routers
  services/         # business logic
  tests/            # request-level tests

Quick start

git clone https://github.com/YOUR-USERNAME/express-ts-boilerplate
cd express-ts-boilerplate
pnpm i        # or npm/yarn
cp .env.example .env
pnpm dev      # nodemon + ts-node (or tsx) for DX
pnpm test     # jest + supertest
pnpm build    # compile to dist
pnpm start    # run compiled server

What’s “opinionated” here?

  • Errors as classes, not strings — fewer branches, better intent.
  • Request-level tests first — guard contracts, not just internals.
  • Security by default — Helmet/CORS/rate-limits ship enabled.
  • Small, composable middlewares — easy to swap, easy to delete.
  • Minimal dependencies — no mega-framework; just Express + TS.

Roadmap

  • CLI to scaffold modules (route + service + test)
  • Example auth flow (JWT) behind the error model
  • Pluggable logging transports (pretty vs. JSON)

If you want a TypeScript API starter that’s fast to reason about and boring in all the right places, start here and tweak to taste.

Grab the code: https://github.com/kalemmentore868/boilerplate-node-ts-api

pexels-energepic-com-27411-2988232

A Guide to Payment Processors in Trinidad: Which One is Right for You?

Accepting online payments is crucial for businesses in Trinidad, but choosing the right payment processor can be overwhelming. Each gateway has different fees, features, and payout timelines. In this guide, we’ll break down five popular payment processors—WiPay, First Atlantic Commerce (FAC), Scotia Merchant Service, Hexakode Invoicing, and PayPal—so you can decide which one suits your business best.

1. WiPay

WiPay is a great option for businesses looking for a low-cost entry into online payments.

Key Features:

No setup or monthly fees
💰 Transaction fee: 3.5% + $0.25 USD per transaction
💳 Currencies supported: TT & USD
🕒 Payout time: Up to 2 weeks upon withdrawal request
No recurring payment functionality
Ease of setup: Relatively easy
🔌 WooCommerce plugin available: Provided by Wipay itself

💡 Best for: Small businesses or startups looking for a simple and cost-effective way to accept payments online.

2. First Atlantic Commerce (FAC)

FAC is ideal for businesses that need fast payouts and recurring billing but requires a higher investment upfront.

Key Features:

💵 Setup cost: $200 USD
💸 Monthly fee: $175 USD
💳 Transaction fee: Varies (negotiated with your bank)
🕒 Payout time: 1-2 days (funds go directly to your account)
Supports TT & USD transactions (USD requires a USD bank account)
💳 Works with local debit cards when set to TT currency
🛠️ Setup complexity: Requires some technical knowledge
WooCommerce plugin not included, but available for $150 USD/year
Supports recurring payments

💡 Best for: Established businesses that need quick payouts and recurring billing.

3. Scotia Merchant Service

Scotia’s merchant service is a cheaper alternative to FAC with similar features.

Key Features:

💵 Setup cost: $125 USD
💸 Monthly fee: $25 USD
💳 Transaction fee: Varies (negotiated with your bank)
🕒 Payout time: 1-2 days
Supports TT currency
🌎 USD transactions convert to TTD automatically
💳 Works with local debit cards when set to TT currency
🛠️ Requires technical setup
WooCommerce plugin not included, but available for $150 USD/year
Supports recurring payments

💡 Best for: Businesses looking for an affordable and reliable local payment processor.

4. Hexakode Invoicing

Hexakode Invoicing is a hassle-free option with no monthly costs and built-in recurring payments.

Key Features:

💵 No setup or monthly fees
💰 Transaction fee: 6% per transaction
🕒 Payout time: 1-5 business days upon withdrawal request
Supports TT currency
💳 Works with local debit cards
Ease of setup: Relatively easy
🔌 WooCommerce plugin included
Supports recurring payments

💡 Best for: Freelancers and small businesses looking for an easy-to-use invoicing and payment solution.

5. PayPal

PayPal is widely recognized and great for international transactions, but it has some drawbacks for local businesses.

Key Features:

💵 No setup or monthly fees
💰 Transaction fee: 5.5% per transaction
🌎 Only supports USD (users must have a credit card)
💳 Withdrawing funds can be difficult (requires a credit card or JMMB debit card)
Supports recurring payments
🔌 WooCommerce plugin included

💡 Best for: Businesses selling to international customers who can pay in USD.


Conclusion: Which Payment Processor Should You Choose?

Choosing the right payment processor depends on your business needs:

  • For a low-cost, easy setup: WiPay or Hexakode Invoicing
  • 🔄 For recurring payments & fast payouts: FAC or Scotia Merchant Service
  • 🌍 For international sales: PayPal

Each option has its pros and cons, so it’s important to pick the one that aligns with your business model and cash flow needs.

📢 Need more guidance? Get a free consultation to find the best payment gateway for your business! Sign up for our newsletter at Hexakode Agency and check out our YouTube channel for a more detailed breakdown! 🚀


💬 Which payment processor do you use? Let us know in the comments below!