`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.
- `acquisitions` makes security a runtime decision, not a static wrapper, by changing request treatment based on identity and role.
- Its strongest move is not the library list, but the way Arcjet, validation, and service boundaries line up into a trust chain.
- The repo narrows untrusted input early, selects safe fields deliberately, and keeps database details out of controllers.
- The Docker and deployment defaults signal a boilerplate meant for real services, not throwaway demos.
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.
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.
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.
| Dimension | Typical boilerplate | acquisitions |
|---|---|---|
| Security enforcement | Static middleware and generic auth guards | Arcjet policy that changes with request identity |
| Rate limiting model | One blanket limiter for everyone | Role-aware budgets for guests, users, and admins |
| Data exposure risk | Loose selects or broad response shaping | Explicit field selection and safer service boundaries |
| Validation strategy | Ad hoc checks in controllers | Zod schemas that narrow input before business logic |
| Deployment maturity | Local dev first, production later | Multi-stage Docker builds, non-root runtime, health checks |
| Code organization | Conventional Express folders without strong boundaries | Layered 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 case | Typical boilerplate | acquisitions |
|---|---|---|
| Toy app or prototype | Usually enough | Probably more system than you need |
| SaaS backend | Often missing safeguards | Good fit for layered trust and abuse control |
| Internal admin tool | Works, but can be loose | Strong match for role-based policy |
| API under real traffic | Rate limits and auth are often bolted on | Security and throughput are designed together |