`acquisitions`: The Node.js API Boilerplate That Treats Security Like an Execution Layer

A layered Express 5 stack with Arcjet, Drizzle, Zod, and Docker defaults that turns user role, bot detection, and rate limiting into part of the request flow.

6 to 8 min read • View on GitHub • More from itsokAsh

A wide editorial scene shows three request lanes entering a secured API gate. Guest, user, and admin paths each pass through the same front door, but their throttles differ before traffic reaches the backend. It explains that access control and rate limiting are computed from request identity, not applied as a single blanket rule.
The same request path, but three different security budgets.
Key Takeaways

Most API boilerplates look secure because they have the right middlewares in the right order. `acquisitions` is more interesting. It treats security as something the request path computes, not something the developer vaguely promises in a README.

Why this boilerplate starts with policy, not routes

The big idea is simple: every request arrives with context, and context should change how the API behaves. In this repo, guest, user, and admin traffic do not get the same budget. They are processed through the same stack, but they do not receive the same throughput or scrutiny.

A close-up shows a raw URL parameter and an authentication token moving through a validation funnel. The ID becomes a typed value, the role is checked, and the service layer returns only safe fields while a password hash is kept out of the response path. It explains how the app narrows trust at each boundary.
A request is not accepted all at once. It is narrowed step by step.

The security layer that changes shape per request

The repository’s standout move is the Arcjet middleware. It combines shield, detectBot, and slidingWindow protections, then adjusts the rate limit with aj.withRule() based on the caller’s role. That means security policy is not frozen at startup. It is recomputed as the request moves through the app.

import { aj } from '#config/arcjet.js';

export const securityMiddleware = async (req, res, next) => {
  const role = req.user?.role || 'guest';

  const rule = role === 'admin'
    ? { max: 20, window: '1m' }
    : role === 'user'
      ? { max: 10, window: '1m' }
      : { max: 5, window: '1m' };

  const decision = await aj.withRule(rule).protect(req);

  if (decision.isDenied()) {
    return res.status(429).json({ message: 'Too many requests' });
  }

  next();
};

That matters because it changes what the API is optimizing for. A static limiter protects the server. A role-aware limiter protects the server while acknowledging that an authenticated admin is not the same as an anonymous scraper.

acquisitions. June 10, 2026 – Present. acquisitions — GitHub repository.

OpenTalent Profile for Ashish Kumar, Project Listing · Ashish Kumar | OpenTalent | OpenTalent

How the request becomes a trusted object

The validation layer keeps the trust chain tight. In the user ID schema, a URL parameter starts as a string, gets checked against a digit-only regex, and is then transformed into a number. That is not just tidiness. It prevents the rest of the app from pretending untrusted data is already safe.

import { z } from 'zod';

export const userIdSchema = z.object({
  id: z.string().regex(/^\d+$/).transform(Number)
});

That pattern pays off in controllers. By the time a service sees the value, the type is narrower and the failure mode is already defined. The controller is no longer a place where raw request data survives longer than it should.

The service layer refuses to leak more than it needs to

The service layer is equally deliberate. It does not spray raw database rows around the app. It selects only what the caller needs, and it checks for email uniqueness before attempting an update. That is a cleaner line of defense than hoping a database constraint error will be informative enough later.

export const getUserById = async (id) => {
  const user = await db.select({
    id: users.id,
    name: users.name,
    email: users.email
  }).from(users).where(eq(users.id, id));

  return user[0] || null;
};

export const updateUser = async (id, data) => {
  const existing = await db.select({ id: users.id })
    .from(users)
    .where(and(eq(users.email, data.email), ne(users.id, id)));

  if (existing.length > 0) {
    throw new Error('Email already in use');
  }

  return db.update(users).set(data).where(eq(users.id, id));
};

The result is a stronger privacy boundary. Controllers stay thin, models stay focused, and password hashes never become accidental API surface.

The stack choices that make the boilerplate feel current

The repo is not trying to invent a new framework. It uses Express 5, native ES modules, path aliases, Drizzle, and Neon to make the existing stack cleaner and more explicit. That is the right kind of modern: less ceremony, fewer leaky imports, and a better contract between files.

DimensionTypical boilerplateacquisitions
Security enforcementStatic middleware and generic auth guardsArcjet policy that changes with request identity
Rate limiting modelOne blanket limiter for everyoneRole-aware budgets for guests, users, and admins
Data exposure riskLoose selects or broad response shapingExplicit field selection and safer service boundaries
Validation strategyAd hoc checks in controllersZod schemas that narrow input before business logic
Deployment maturityLocal dev first, production laterMulti-stage Docker builds, non-root runtime, health checks
Code organizationConventional Express folders without strong boundariesLayered models, services, controllers, middleware, and config

Production maturity shows up in the Dockerfile

The Dockerfile pushes the same theme into deployment. Multi-stage builds keep the runtime lean, the container runs as a non-root user, and the health check verifies the app from inside the image. That is not window dressing. It is the difference between a repo that demos cleanly and one that can survive a real deployment pipeline.

What this boilerplate is really for

`acquisitions` is best read as a trust architecture for APIs that cannot afford to be casual. It fits SaaS backends, internal tools, and any service where abuse prevention, auth, and deployment hygiene matter as much as feature speed. Compared with a typical boilerplate, it asks a harder question: what should this request be allowed to do, and how much should we trust it at each step?

Use caseTypical boilerplateacquisitions
Toy app or prototypeUsually enoughProbably more system than you need
SaaS backendOften missing safeguardsGood fit for layered trust and abuse control
Internal admin toolWorks, but can be looseStrong match for role-based policy
API under real trafficRate limits and auth are often bolted onSecurity and throughput are designed together