nodejs-best-practices — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nodejs-best-practices (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The aim here is judgment, not recipes. Identical projects rarely exist, so resist reaching for the same stack and layout every time. When the requirements leave a choice open, surface the trade-off and ask before committing.
Start from where the code will run and how much structure the team needs, then narrow:
Questions worth answering before you decide: Where does this deploy? Does cold-start latency show up in the SLO? What has the team shipped before? Is there existing code that constrains the choice?
TypeScript without a build step. Recent Node LTS strips types from .ts files on the fly, so node server.ts runs directly. The catch: only erasable syntax works -- enum, runtime namespace, and constructor parameter properties will throw. When you need those, run through tsx instead. This is handy for one-off scripts and lean services.
Modules. New code should be ESM (import/export) -- it tree-shakes better and is the direction the ecosystem is moving. Stick with CommonJS (require) only when an existing codebase or a stubborn dependency forces it.
Which runtime. Node remains the safe default with the deepest ecosystem. Bun is worth a look when raw speed and an integrated toolchain matter; Deno appeals when you want TypeScript and a permissions model built in.
For anything that will grow, separate concerns into layers so each has one job:
HTTP handler -> parses the request, validates input, returns the response
|
Service -> business rules, framework-agnostic, orchestrates the work
|
Repository -> talks to the database or external store, nothing elseThe payoff is real: layers can be tested in isolation, the data store can be swapped without rewriting logic, and responsibilities stay legible. That said, a 40-line script or a throwaway prototype does not need this -- before adding structure, ask whether the thing is likely to outlive the week.
Centralize it. Define your own error classes, throw them from wherever the problem is detected, and catch them once at the top (an error-handling middleware). The client and the logs see different things on purpose:
Map situations to status codes deliberately:
| Situation | Code |
|---|---|
| Malformed or invalid input | 400 |
| Missing/invalid credentials | 401 |
| Authenticated but not permitted | 403 |
| Resource absent | 404 |
| State or uniqueness conflict | 409 |
| Schema-valid but breaks a business rule | 422 |
| Our fault -- log everything | 500 |
Match the construct to the shape of the work:
await in sequence when each step depends on the last.Promise.all to fan out independent calls and wait for all.Promise.allSettled when some of those calls are allowed to fail.Promise.race for timeouts or first-wins responses.Keep the event loop in mind. Async buys you nothing for CPU-bound work -- it only helps while the process waits on I/O:
Helped by async (waiting): DB queries, HTTP calls, disk reads, sockets
Not helped (computing): hashing, image work, heavy math
-> move these to worker threads or a separate serviceTwo rules that prevent most stalls: never call the synchronous variants (readFileSync and friends) on a hot path, and stream large payloads instead of buffering them whole.
Validate at every boundary where untrusted data enters: incoming request bodies and params, anything about to hit the database, responses from third-party APIs, uploaded files, and environment variables at startup. Treat "internal" data as untrusted too.
For the validator itself: Zod is the TypeScript-first default with strong inference; Valibot wins when bundle size is tight thanks to tree-shaking; ArkType targets the performance-critical path; Yup fits when it already lives in a React form. Whatever you pick, fail early and return messages specific enough to act on.
Walk this list on every service:
The underlying stance: trust nothing crossing the boundary -- query strings, bodies, headers, cookies, uploads, and external API responses all get checked.
Spend effort where failure hurts:
| Layer | Focus | Typical tools |
|---|---|---|
| Unit | business logic | node:test, Vitest |
| Integration | endpoints end to end | Supertest |
| E2E | full user flows | Playwright |
Prioritize critical paths (auth, payments, core flows), then edge cases (empty inputs, boundaries), then failure handling. Skip the trivia -- framework internals and one-line getters do not need tests. Node's built-in runner (node --test) covers most unit needs with no extra dependency, plus watch mode and coverage.
Avoid: defaulting Express onto a fresh edge project, synchronous I/O in production, business logic stuffed into handlers, skipped validation, hard-coded secrets, trusting external data unchecked, and blocking the loop with CPU work.
Prefer: choosing the framework for the situation at hand, asking about preferences when they are unclear, layering anything that will grow, validating everywhere input arrives, sourcing secrets from the environment, and profiling before you optimize.
Every project earns a fresh look. The patterns are scaffolding for thinking, not answers to copy.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.