railguard

An npm package for Node.js

railguard

Guardrails for Node.js apps, whether a person or an AI wrote the code.

npm install railguard

Along the line

Five places it catches mistakes

Code passes these checkpoints in this order, from the function you just wrote to the request a user sends in production. railguard puts a guard at each one.

  1. Write
    railguard probe

    Feeds null, empty strings, NaN, emoji and invalid dates to your functions and explains every crash.

  2. Test
    assertEdges(fn)

    The same checks inside node:test, Jest or Vitest, so a fixed bug stays fixed.

  3. Commit
    railguard hook

    Blocks commits that contain database passwords, API keys or .env files.

  4. Boot
    guard(schema)

    The app refuses to start with a missing or mistyped .env, and lists every problem at once.

  5. Request
    errorHandler()

    Every error leaves as the same JSON shape. No stack traces in production.

Real output

Three lines, three ways to crash

This getInitials looks finished. The terminal output below comes from actual runs of railguard, trimmed only for length.

export function getInitials(fullName) {
  return fullName.split(' ').map((part) => part[0].toUpperCase()).join('')
}
$ npx railguard probe src/utils.js getInitials
railguard probe · getInitials(string)
  36 cases · 23 passed · 13 failed (4 distinct problems) · 0 rejected on purpose

  ✗ empty string, single space, whitespace only, leading/trailing spaces
      crash: TypeError: Cannot read properties of undefined (reading 'toUpperCase')
      fix: add a null/undefined check or a default value

  ✗ undefined, null
      crash: TypeError: Cannot read properties of undefined (reading 'split')
      fix: add a null/undefined check or a default value

  ✗ wrong types: number 42, boolean true, array, object, BigInt 10n, function
      crash: TypeError: fullName.split is not a function
      fix: check the type before calling a method on it
  …

$ npx railguard probe src/utils.js getInitialsSafe
railguard probe · getInitialsSafe(string)
  36 cases · 36 passed · 0 failed · 0 rejected on purpose

  ✓ no crashes found

All examples use throwaway demo values, not real credentials. In the errorHandler tab the response bodies are verbatim from a test app; the request paths are simplified.

One install, three guards

What it checks

Env and secrets

Describe your environment once. railguard checks it at startup, turns strings into real numbers and booleans, and keeps your secrets out of git.

  • A missing MONGO_DB or a PORT of "five-thousand" stops the app at boot, not on the first request.
  • A typo like env.MONGO_URL throws instead of quietly returning undefined.
  • console.log(env) prints [secret] for passwords and tokens.
  • Database URLs with passwords, Gmail app passwords, AWS, GitHub, Stripe, OpenAI and Anthropic keys, JWTs and private keys are blocked at commit.
  • A .env that isn't git-ignored, or is still tracked after you ignored it, is flagged.
// env.schema.js (npx railguard init writes it from your .env.example)
const { t } = require('railguard')

module.exports = {
  PORT: t.port().default(5000),
  MONGO_DB: t.url(),
  JWT_SECRET: t.string({ min: 32 }).secret(),
  SENDER_EMAIL: t.email(),
  NODE_ENV: t.enum(['development', 'production']),
}

// server.js
const env = guard(require('./env.schema'))
app.listen(env.PORT)   // a number, not "5000"

Express errors and validation

Throw from any route and the client gets one predictable JSON shape. It already knows the errors a MERN app throws, with no extra dependencies.

ThrownSent
Mongoose ValidationError400 VALIDATION_ERROR
Bad ObjectId (CastError)400 INVALID_ID
Duplicate key E11000409 DUPLICATE
Expired or invalid JWT401 TOKEN_EXPIRED
File too large (multer)413
Anything else, in production500, generic message
const { t, validate, errorHandler, notFound, ok, created, HttpError } = require('railguard')

app.post('/api/users',
  validate({ body: { name: t.string({ min: 1 }), email: t.email() } }),
  async (req, res) => created(res, await User.create(req.body))
)

// Express 5 passes async errors on by itself. On Express 4, wrap with asyncHandler().
app.get('/api/lessons/:id', async (req, res) => {
  const lesson = await Lesson.findById(req.params.id)
  if (!lesson) throw HttpError.notFound('Lesson not found')
  ok(res, lesson)
})

app.use(notFound())
app.use(errorHandler())

Edge cases

AI assistants and tired humans both write for the happy path. probe tries the inputs nobody tested, one argument at a time, and says how to fix each crash.

  • TypeError: Cannot read properties of undefined and the other accidental crashes.
  • Functions that return NaN, an Invalid Date, or text containing undefined or [object Object].
  • Functions that change the array or object they were given.
  • Prototype pollution from {"__proto__": …} payloads.
  • Async functions that never settle.

Errors you throw on purpose, like throw new TypeError('email must be a string'), don't count as crashes.

import { test } from 'node:test'
import { assertEdges, edge } from 'railguard/edge'

test('getInitials survives edge cases', () =>
  assertEdges(getInitials, { args: ['string'] }))

test('formatPrice', () =>
  assertEdges(formatPrice, {
    args: ['number', edge.custom('USD', ['', 'usd'])],
    allow: (err) => err instanceof RangeError,  // thrown on purpose
    invariant: (out) => typeof out === 'string',
  }))

For Claude, Copilot, Cursor and friends

Make your AI assistant an honest reviewer

AI tools write code fast, skip edge cases, and rarely warn you before you leak something. One command writes rules into the files your assistant reads: AGENTS.md, CLAUDE.md, .cursorrules or Copilot's instructions.

npx railguard ai

From then on, your assistant tells you when a secret is exposed, checks edge cases before calling code done, and walks you through a checklist before you ship. The same rules open the package's llms.txt.

AGENTS.md · excerpt
## Working rules for AI assistants (railguard)

Act as the honest senior reviewer on this project. When something is unsafe, broken or unfinished, say so plainly in one sentence with the reason and the fix, even if the user didn't ask.

### Secrets
- If the user pastes a real secret into the chat, tell them right away that it is exposed and must be rotated. Don't repeat it back.
- Never hard-code secrets. Never suggest posting .env contents, logs or screenshots that show secrets.
- Before any commit or push, `npx railguard scan` must exit 0.

### Edge cases
- Before calling a function done, think through null/undefined, empty input, wrong types, NaN, Unicode, invalid dates and async failures. Run `npx railguard probe` and fix every ✗.

### Before shipping, walk the user through
1. Tests pass.  2. `railguard check`.  3. `railguard scan`.
4. Secrets in the host's settings.  5. `npm audit`.

Know the limits

What it won't do

Find every edge case

No tool can. It covers the inputs that most often crash JavaScript. For your business rules, add an invariant.

Stop a sync infinite loop

JavaScript can't interrupt one. Async hangs are caught with a timeout. probe runs the file you point it at, so use it on utilities, not your server entry.

Replace bigger tools

zod, gitleaks and fast-check go deeper. railguard is the small, zero-config first line, with output a beginner can act on.