Skip to main content
Back to module
Software Engineering Practices · Running Software

Secure development

Reading48mLesson 2 of 3 in this module

Keep secrets out of Git, keep dependencies patched, and put security checks in CI.

Why it matters

Most security incidents in ordinary web apps do not come from brilliant attackers. They come from a handful of everyday mistakes: an API key committed to a public repository, a dependency with a known vulnerability that nobody updated, user input passed straight into a query. Secure development means building habits and automated checks so those mistakes are caught before they reach production. This lesson focuses on two of the most common problems, secrets and dependencies, and the free tools that catch them. The Back-End Web APIs course covered input validation, password hashing and the OWASP API Top 10; the same ideas apply here.

Secrets: never in the code

A secret is anything that grants access: API keys, database passwords, signing keys for tokens, cloud credentials. The rule is simple: secrets never go in source code or in Git.

// Wrong: the key is in the code, in every clone, and in Git history forever.
const payments = createClient("live-key-1234-abcd");

// Right: read it from the environment, and fail loudly if it is missing.
const key = process.env.PAYMENT_API_KEY;
if (!key) throw new Error("PAYMENT_API_KEY is not set");
const payments = createClient(key);
  • In development, keep values in a .env file that is listed in .gitignore, and commit a .env.example with the variable names but no values.
  • In production, set them in your host's environment settings or a secret manager.
  • Give each environment its own secrets, and give each key the smallest permissions it needs.

If a secret is ever committed, treat it as leaked, even if you delete the line in the next commit. The old commit is still in history, and bots scan public GitHub for keys within minutes. The fix is to rotate it: create a new key in the provider's dashboard, deploy the new one, and revoke the old one. Rewriting Git history afterwards is optional clean-up, not the fix.

GitHub helps here. Secret scanning detects known key formats in repositories, and push protection blocks a push that contains one before it lands. Both are free for public repositories.

Dependencies: the code you did not write

A typical Node project has a few direct dependencies and hundreds of indirect ones. Each is code that runs with your app's permissions. Risks include known vulnerabilities (published as CVEs and security advisories) and supply-chain attacks, where a package or one of its maintainers' accounts is compromised and a malicious version is published.

Habits that keep the risk manageable:

  • Commit the lock file (package-lock.json) and install with npm ci in CI, so every build uses exactly the reviewed versions.
  • Audit regularly. npm audit lists known vulnerabilities in your installed tree with their severity. npm audit fix applies compatible upgrades.
npm audit --audit-level=high   # exits non-zero if any high or critical advisory exists
  • Automate updates. Dependabot (free on GitHub) opens pull requests that bump vulnerable or outdated dependencies. Your CI runs the tests on each one, so you can merge with confidence.
  • Add dependencies deliberately. Before installing, check that a package is maintained, widely used and actually needed. A few lines of your own code can beat a tiny package with a large dependency tree.
  • Read the advisory before panicking. Not every finding is exploitable in your app; a vulnerability in a build tool that never touches user input is lower priority than one in your request parser. Fix the reachable ones first.

Put the checks in CI

Checks that depend on someone remembering will be forgotten. Make them part of every pull request. A minimal GitHub Actions workflow, free for public repositories:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test
      - run: npm audit --audit-level=high

Other secure-coding basics

  • Validate input at every boundary and use parameterised queries, never string-built SQL.
  • Escape output in HTML (frameworks like React do this by default; avoid bypassing it).
  • Check permissions on the server for every request, not just in the UI.
  • Fail closed: if a security check errors, deny access.
  • Keep secrets and personal data out of logs and error messages.

Common mistakes

  • "It's a private repo, so a key in the code is fine." Private repos get cloned, forked, shared and sometimes made public.
  • Deleting a leaked key from the code without rotating it.
  • Ignoring audit output for months until there are hundreds of findings.
  • Merging Dependabot PRs without tests, or never merging them at all.
  • Installing a package with a misspelled name (typosquatting).

Recap

  • Secrets live in environment variables or a secret manager, never in Git; a committed secret must be rotated.
  • Lock files, npm ci, npm audit and Dependabot keep dependencies under control.
  • Put tests and audits in CI so every pull request is checked automatically.

Try it

In the Do tab, set up secret handling, dependency scanning and a CI workflow on a free GitHub repository.

Also in this lesson: