16.9 C
New York
Thursday, November 5, 2026

Categorical.js + TypeScript API Tutorial: Constructing a RESTful API


Selecting a stack for a REST API again finish normally means deciding between a minimal, unopinionated framework, equivalent to Categorical with TypeScript, and an opinionated one like FastAPI. Categorical and TypeScript are a robust alternative when management, incremental adoption, and a small abstraction layer matter greater than the built-in conventions that include an opinionated stack like FastAPI with Python. Moreover, Categorical and TypeScript are mature, broadly understood, and helpful for small companies, established techniques, and weird integrations. TypeScript compensates for a few of Categorical’s looseness by implementing contracts contained in the codebase, and it allows you to attract in your preexisting JavaScript familiarity.

However there’s a catch. Categorical solely supplies routing and middleware. Choices about validation, dependency boundaries, error dealing with, logging, and testing are left to the event crew. That association could also be manageable on a small challenge, however on a greenfield challenge with a number of crew members it turns into expensive. With out shared conventions, builders find yourself fixing the identical issues in several methods, and choices that ought to take minutes typically flip into prolonged debates.

That’s why I wrote this information. Drawing on classes from constructing APIs at Microsoft and a number of other small startups, I lay out a working set of conventions for an Categorical REST API. I’ll cowl authentication, runtime validation, MongoDB persistence, and testing, and I’ll present an entire overview of how a request goes from path to database.

What Is an Categorical TypeScript API?

An Categorical TypeScript API combines three concepts:

  • Categorical maps HTTP requests to middleware and handlers whereas leaving construction to the developer.
  • TypeScript checks compile-time contracts amongst information switch objects (DTOs), companies, and information entry code.
  • REST exposes assets by way of stateless HTTP operations equivalent to GET /customers/:id, POST /customers, PATCH /customers/:id, and DELETE /customers/:id.

Notably, TypeScript doesn’t validate HTTP enter. Request our bodies, question strings, headers, webhooks, and even database outcomes can violate TypeScript assumptions, so your app must validate untrusted information earlier than the enterprise logic stage.

What You’ll Construct and Stipulations

In constructing an API with Categorical that has authentication, authorization, validation, and database integration, you’ll create a request life cycle that passes management from layer to layer: route, middleware, controller, service, information entry objects (DAO), and eventually MongoDB.

You’ll must have these put in:

  • Node.js 24 LTS
  • npm (or one other package deal supervisor)
  • MongoDB
  • An API consumer (equivalent to curl or Postman)

You’ll additionally want fundamental familiarity with the command line and Git.

A production-ready API begins with a contemporary, reproducible growth baseline; you’ll be able to keep away from complications by preserving tooling and configuration predictable throughout native growth, CI, and manufacturing environments. Apart from Node.js 24 LTS, this information makes use of native environment-file assist, strict TypeScript, ESLint, and security-focused linting.

In right now’s fast-changing panorama, it’s possible you’ll come throughout outdated Node.js/Categorical.js/TypeScript patterns in instance repos or AI-generated code; ESLint helps mitigate this to some extent.

When you’ve got a necessity for pace, take into account Biome:

  • It’s a worthy and way more performant alternative for ESLint.
  • It additionally replaces Prettier’s performance (coated beneath), eliminating a dependency.
  • It’s already appropriate with TypeScript 7, which itself is far sooner than TypeScript 6.

The principle draw back, nevertheless, is that Biome can not leverage ESLint’s intensive plugin system, together with the safety plugin this Categorical/TypeScript tutorial makes use of. You’d then want to seek out one other dependency or software to fill that hole.

Challenge Initialization and Dependencies

Create the challenge inside npm init -y, then set up (npm i) manufacturing packages individually from their TypeScript sort add-ons and different growth (-D) dependencies:

npm i categorical@^5 zod@^4 mongoose@^9 argon2 jsonwebtoken helmet 
  express-rate-limit cors pino pino-http
npm i -D typescript@^6 tsx @varieties/node @varieties/categorical @varieties/cors 
  @varieties/jsonwebtoken eslint typescript-eslint prettier 
  eslint-config-prettier eslint-plugin-security

Commit the lockfile reasonably than copying transient patch variations into software code.

TypeScript 6 is the newest ESLint-compatible transpiler. If you could run it manually and don’t wish to put tsc in your path, you should use npx tsc, however when you configure the Node.js package deal.json that you just simply generated with npm init, you received’t must.

TypeScript Configuration

In package deal.json, set "sort": "module" to make use of trendy ESM import syntax as an alternative of the default CommonJS and add some helpful scripts by changing "scripts": {...}, with:

"sort": "module",
"scripts": {
  "dev": "tsx watch --env-file=.env src/server.ts",
  "typecheck": "tsc --noEmit",
  "construct": "tsc",
  "begin": "node --env-file=.env dist/server.js",
  "take a look at": "node --test --import=tsx "take a look at/**/*.take a look at.ts"",
  "lint": "eslint .",
  "audit": "npm audit --omit=dev --audit-level=excessive"
},

Manufacturing secrets and techniques belong within the deployment platform’s secret supervisor, so hold .env out of model management utilizing .gitignore. (You’ll be able to at all times doc required variables and any native and/or nonsecret defaults with a version-controlled .env.instance.)

Use strict TypeScript settings and separate supply from output by placing this in a root tsconfig.json:

{
  "compilerOptions": {
    "goal": "ES2025",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "sourceMap": true
  },
  "embody": ["src/**/*.ts"]
}

It’s price contemplating using aliases from the start, which may take among the busywork out of any folder-level refactoring you may select to do later.

Linting, Formatting, and Atmosphere Setup

You should utilize really useful TypeScript linting, safety, and Prettier formatting settings by placing the next in eslint.config.js within the challenge root:

import tseslint from 'typescript-eslint';
import safety from 'eslint-plugin-security';
import prettierConfig from 'eslint-config-prettier';

export default tseslint.config(
  ...tseslint.configs.really useful,
  {
    plugins: { safety },
    guidelines: {
      ...safety.configs.really useful.guidelines,
    },
  },
  prettierConfig,
);

Safety lint guidelines are helpful indicators, however beware that they don’t show that code is safe, and placing prettierConfig final offers it priority for consistency with business formatting norms.

As for environments, you’re already in fine condition: The native --env-file strategy from the earlier part is extra broadly appropriate than shell-specific export scripts.

Challenge Construction and API Structure

Categorical supplies API constructing blocks; you determine the best way to manage them. However should you let database queries, validation, and enterprise guidelines accumulate inside route handlers, upkeep turns into painful and even dangerous.

Your primary countermeasure is to separate transport considerations (e.g., the HTTP layer) from software logic. This creates clearer boundaries, makes automated testing of particular person layers simpler, and leaves room to prepare the code by characteristic as the appliance grows.

Folder Construction

For a compact service, manage by technical layer:

mkdir -p src/{routes,controllers,companies,information,dto,utils,middleware}

As the appliance grows, recreate that construction below every characteristic listing (e.g., mkdir -p src/{customers,auth}/{routes,controllers,...}) whereas preserving the identical boundaries.

It’s not mere conference: It retains transport code, enterprise choices, and infrastructure independently testable.

Layered Design (Routes, Controllers, Providers, DAO, DTO)

A scalable structure typically entails these layers (typically extra):

It’s a code scent to cross layers. For those who discover Categorical varieties in your companies or database specifics in your controllers, refactor them.

Constructing the API (Server, Routes, Controllers, Knowledge Layer)

Wiring these layers right into a working request pipeline, whereas preserving every layer minimal and targeted, makes the entire request path simpler to purpose about, take a look at, and alter. However you’ll nonetheless want an app entry level to kick-start the Categorical pipeline.

Creating the Categorical Server

It’s typically finest to load configuration earlier than bootstrap and hook up with the database earlier than listening, failing early so your server doesn’t find yourself in an unexpected state if atmosphere variables are lacking or exterior companies are down.

International middleware runs within the order it’s outlined, so put basic safety upfront and error-handling final:

import categorical from "categorical";
import helmet from "helmet";
import pinoHttp from "pino-http";
import mongoose from "mongoose";
import { usersRouter } from "./routes/customers.routes.js";
import { errorHandler } from "./middleware/error.middleware.js";

if (!course of.env.MONGODB_URI) throw new Error("MONGODB_URI required");
const app = categorical();
app.use(helmet(), categorical.json({ restrict: "100kb" }), pinoHttp());
// Allow CORS solely when browser purchasers want it; use an express origin allowlist.
app.use("/customers", usersRouter);
app.use(errorHandler); // final

await mongoose.join(course of.env.MONGODB_URI);
app.pay attention(Quantity(course of.env.PORT ?? 3000));

For rising groups, think about using @natebourgoin/requiredenv (or your cloud supplier’s key vault) to handle atmosphere necessities; as for companies which might be down regardless of being appropriately configured, you’ll be able to deal with connection failures with bounded retries and exponential backoff. For instance, Mongoose doesn’t retry failed connections on startup, even when it’s going to retry a profitable connection that later fails.

It’s additionally finest to deal with SIGTERM gracefully: Cease accepting requests, drain in-flight work queues, shut MongoDB, and exit.

Routing and Controllers

Outline one Categorical Router per useful resource (customers, auth) and hold their route modules declarative (i.e., merely wiring middleware and controllers). Validation and authentication middleware ought to run earlier than the controller so it receives already-validated enter; it then calls a single enterprise operation within the service layer, chooses the HTTP standing, and serializes the outcome.

Knowledge Entry (DAO Sample)

DAOs encapsulate queries, persistence mapping, and database-specific identifiers. They return typed area entities reasonably than uncooked driver responses. This prevents, for instance, MongoDB particulars equivalent to _id and __v from leaking into each layer.

It’s a typical mistake to succeed in for @ts-ignore for nullable outcomes. As a substitute, let TypeScript slender the sort with guards like if (!person) throw new NotFoundError("Consumer"); (assuming you derived a customized NotFoundError class from Error), which lets the transpiler deduce that person can’t be null after that line. Likewise, for actually dynamic outcomes, typically Document<string, string> is as slender as you may get, however that’s already higher than @ts-ignore.

Validation, Middleware, and Error Dealing with

Crosscutting considerations like validation belong in reusable middleware. This avoids duplication and enforces predictable enter dealing with and response shapes, preserving controllers targeted.

DTOs and Zod Validation

A DTO does runtime validation and returns typed information. It ought to implement construction and native constraints, not question the database or determine enterprise coverage.

Zod permits you to outline a given schema as soon as for all layers, then infer its DTO:

import * as z from "zod";

export const createUserBody = z.strictObject({
  e mail: z.e mail(),
  password: z.string().min(12).max(128),
  title: z.string().trim().min(1).max(100)
});
export sort CreateUserDto = z.infer<typeof createUserBody>;
export const createUserRequest = z.object({
  physique: createUserBody,
});

Right here, the createUserRequest envelope makes it simpler for middleware to validate whichever mixture of physique, params, and question it requires.

Reusable Middleware

The envelope strategy lets particular person routes validate whichever components of the request they require by way of reusable middleware, which then locations the parsed, validated, typed end in res.locals:

export const validate =
<T>(schema: z.ZodType<T>) =>
  (req: Request, res: Response, subsequent: NextFunction) => {
    const outcome = schema.safeParse({
    physique: req.physique,
    params: req.params,
    question: req.question,
  });

  if (!outcome.success) {
  return subsequent(new ApiError(400, "Invalid request", outcome.error.points));
  }

  res.locals.enter = outcome.information;
  subsequent();
};

This retains inline validation out of controllers and rejects surprising physique fields. Authorization stays separate, since legitimate enter is just not essentially permitted enter.

Apply the identical validator manufacturing unit to request our bodies, path parameters, and question values. Authentication, request ID-stamping, and price limiting are additionally middleware; every ought to do one job and both name subsequent() or finish the response.

Centralized Error Dealing with

Categorical 5 (not like 4) forwards rejected guarantees from async handlers to error middleware, neatly avoiding the repetitive controller attempt–catch blocks that had as soon as been obligatory for async approaches. Outline software errors with standing fields and safe-to-return particulars, then normalize each failure response as { error: { standing, message, particulars } } in a single remaining four-argument middleware operate:

export operate errorHandler(err: unknown, req: Request, res: Response, _next: NextFunction) {
  const error = err instanceof ApiError ? err : new ApiError(500, "Inside error");
  req.log.error({ err, code: error.code }, "request failed");
  res.standing(error.standing).json({
    error: { standing: error.standing, message: error.message, particulars: error.particulars }
  });
}

You’ll be able to prolong this (and your throw statements) to leverage commonplace error.trigger semantics for higher logging with out altering that normalized response form.

Authentication and Safety

To maintain customers protected from on-line assaults, manufacturing safety requires making certain {that a} request accommodates a legitimate token. Passwords will need to have protected storage, authentication middleware should set up the caller’s id, and authorization should stay a separate enterprise choice. Contemplating the prevalence of automated hacking, defending towards frequent HTTP and credential assaults and following finest practices for secret administration is now not a “good to have.”

JWT Authentication and Password Hashing

When a person registers, hash their password with Argon2id and retailer solely the encoded hash. At login, confirm the hash and signal a short-lived JWT containing a steady person ID as its sub (topic):

const accessToken = jwt.signal({ sub: person.id }, jwtSecret, {
  expiresIn: "15m", issuer: "users-api", viewers: "users-api-client"
});
res.json({ accessToken });

Authentication middleware verifies the token and establishes the authenticated id for subsequent requests, whereas service-level authorization nonetheless decides whether or not that person might act on a useful resource.

Safety Finest Practices (Helmet, Fee Limiting, Secrets and techniques)

It’s finest to make use of an applicable refresh-token technique, apply Helmet to all HTTP headers, serve solely over TLS, and by no means return stack traces with errors.

All the time shield key routes (e.g., login and restoration) with price limits:

const loginLimiter = rateLimit({ windowMs: 15 * 60_000, restrict: 10 });
authRouter.publish("/customers/login", loginLimiter, validate(loginRequest), loginController);

However when a number of software cases should implement one coverage, improve to a shared limiter retailer.

As for secrets and techniques, by no means hardcode JWT keys or database credentials, and rotate keys by way of managed secret storage.

Database Integration (MongoDB)

Database-specific queries and doc mapping occur within the DAO, permitting your API to make use of persistent storage with out controllers or companies having any consciousness of MongoDB specifics.

Connection Setup

As talked about earlier, it’s finest to attach, dealing with errors with retries, earlier than accepting visitors. This strategy helps hold the MongoDB URI and connection choices remoted from controllers, companies, and DAOs.

CRUD Instance

Use a Mongoose schema for storage and a DAO to map paperwork into area objects:

export const usersDao = {
  async create(enter: Omit<CreateUserDto, "password"> & { passwordHash: string }): Promise<Consumer> {
    const doc = await UserModel.create({ _id: crypto.randomUUID(), ...enter });
    return { id: doc._id, e mail: doc.e mail, title: doc.title };
  },
  async byId(id: string): Promise<Consumer> {
    const doc = await UserModel.findById(id).lean();
    if (!doc) throw new NotFoundError("Consumer");
    return { id: doc._id, e mail: doc.e mail, title: doc.title };
  }
};

It’s left as a reader train to equally implement, replace, and delete performance on this layer utilizing Mongoose’s findByIdAndUpdate and findByIdAndDelete, respectively. Controllers ought to by no means rely on uncooked MongoDB responses.

Testing, Logging, and Dependency Administration

A manufacturing API wants reliability and maintainability because the service adjustments over time. Logging provides the observability wanted to debug and confirm conduct; automated checks are there to constantly implement it. It’s not simply your codebase that evolves: All of its dependencies do, exposing your challenge to supply-chain assaults should you replace dependencies too typically, and third-party vulnerabilities should you don’t replace them typically sufficient. Audit them typically.

Node Take a look at Runner

It can save you a dependency through the use of the built-in node:take a look at runner for unit checks and integration checks organized by characteristic. Begin the app on an ephemeral port, name endpoints with Node’s built-in fetch, and assert standing and response form:

take a look at("POST /customers rejects invalid enter", async () => {
  const token = await getTestAccessToken();
  const response = await fetch(`${baseUrl}/customers`, {
    methodology: "POST",
    headers: { "content-type": "software/json", authorization: token },
    physique: JSON.stringify({ e mail: "not-email" })
  });
  assert.equal(response.standing, 400);
});

Configure your take a look at runner to make use of an remoted take a look at database so that you don’t pollute or destroy manufacturing information.

Structured Logging

Pino supplies structured JSON logs, that are a lot simpler to research than advert hoc console.log or console.debug statements and robotically embody a request’s time stamp, ID, HTTP methodology, standing, and period. For area workflows, log significant state transitions. Usually, by no means log passwords, tokens, authorization headers, cookies, private information, or uncooked request our bodies.

Dependency Auditing

Run npm audit in CI after npm ci; optionally add Snyk or one other scanner. Triage reachability, manufacturing influence, patch availability, and improve threat. Beware {that a} inexperienced scan means no identified problem was detected, not that the availability chain is protected.

Operating and Deploying the API

The appliance ought to behave predictably throughout growth, CI, and manufacturing reasonably than counting on environment-specific workarounds. Maintaining the atmosphere contract constant throughout these phases reduces deployment surprises and makes operational conduct simpler to breed.

Dev vs. Manufacturing Setup

Growth can optimize for quick suggestions with supply maps and scorching reload by way of tsx’s watch mode, whereas manufacturing ought to run compiled output with the one dependencies it wants, use the identical atmosphere contract, expose readiness endpoints, and shut down gracefully.

Finish-to-Finish Instance

A full request walk-through exhibits how layers work together in apply: Middleware handles authentication and validation, the controller delegates the operation, the service applies enterprise guidelines, and the DAO persists the outcome.

Constructing a Full Endpoint

After validation, service delegation, and the database layer, returns bubble management again to the controller to ship the ultimate response to the API client:

// in src/routes/customers.routes.ts:
usersRouter.publish(
  "/",
  requireAuth,
  validate(createUserRequest),
  usersController.create
);

// in src/controllers/customers.controller.ts:
async operate create(
  req: Request,
  res: Response<unknown, { enter: { physique: CreateUserDto } }>
) {
  const person = await usersService.create(res.locals.enter.physique);
  res.standing(201).json({ information: person });
}

// in src/companies/customers.service.ts:
export async operate create(enter: CreateUserDto) {
  const { password, ...profile } = enter;
  const passwordHash = await argon2.hash(password, { sort: argon2.argon2id });
  return usersDao.create({ ...profile, passwordHash });
}

The total move is now: authenticate, validate and normalize, name enterprise logic, hash the password, persist by way of the DAO, map the saved doc, and return a 201 response.

API Finest Practices

The patterns on this information hold an API maintainable as its scope will increase. It’s a typical mistake to be undisciplined about layer boundaries, which turns into a codebase-wide slippery slope. One other pitfall is to downplay operational considerations, which makes retries, timeouts, and background work hidden failure factors.

Code Group and Scalability

To make sure scalability, hold layers remoted, inject dependencies the place checks want substitutes, sure community calls with timeouts, and transfer vital background work to a supervised queue with retries and idempotency keys.

Scaling is extra typically an structure and operations downside than an Categorical efficiency downside. Measure latency percentiles, traces, useful resource utilization, question plans, indexes, timeouts, and exterior calls earlier than including cases. A gradual database question or ambiguous retry boundary is so much simpler to repair than know-how stacks are to vary, and the latter received’t assist with the previous anyway.

Think about a product-ordering workflow. One route validates, authorizes, writes a number of information, prices a cost, sends a notification, and codecs a response. What if there’s an exterior failure after the database writes? Retries can then create duplicates.

That’s why it’s finest to separate transaction boundaries, use idempotency guards for exterior operations, and queue notifications. For those who combine considerations, it’s not merely a method downside; it’s additionally a data-consistency downside. As a substitute, if you strictly adhere to layer boundaries, the following characteristic’s routes and companies are at all times simpler to architect.

Structure, Safety Options, and Scaling Ahead

A maintainable Categorical TypeScript API is express about boundaries. Middleware handles HTTP-wide considerations, with Zod validating untrusted enter, and a centralized error-handler normalizing failures. Controllers translate, companies determine, and DAOs persist information. In the meantime, a production-grade characteristic baseline consists of authentication, safe headers, price limits, secret administration, structured logs, checks, and dependency checks.

As soon as these foundations are in place, the person’s API might be prolonged with pagination, caching, OpenAPI era, metrics and tracing, role- or policy-based authorization, and idempotent background jobs. The trick is so as to add every functionality on the layer that owns it, reasonably than rising route handlers. With that, a small instance Categorical API turns into a sturdy, extensible template with lots of the options generally required in manufacturing and much much less of the technical debt many initiatives accumulate.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles