Skip to content

Security is fun: 10 cybersecurity practices every developer should know

By Adam Hultman · 7 min read

Cybersecurity
Security is fun: 10 cybersecurity practices every developer should know cover image

Security has a reputation for being scary, boring, or something you deal with later when the app is “more real.”

That is how you end up with production secrets in GitHub, admin endpoints protected by spirits, and a database full of plain-text passwords. Spooky stuff.

The good news is that basic security is not mysterious. Most of it comes down to a handful of habits: do not trust user input, do not leak secrets, do not invent your own authentication system, and do not assume nobody will find the weird endpoint you forgot about.

Here are ten practical security habits every developer should build into their workflow.


1. Treat Every API Endpoint Like Someone Will Poke It

APIs are easy to forget about because users usually interact with your nice little frontend. Attackers do not care about your nice little frontend. They will call your endpoints directly.

Every API route should answer a few basic questions:

  1. Who is making this request?
  2. Are they allowed to do this?
  3. Is the input valid?
  4. Could this endpoint be abused if someone called it 10,000 times?

At minimum, use HTTPS, require authentication where needed, check authorization on sensitive actions, validate all input on the server, and rate-limit endpoints that can be spammed.

Also, never hardcode API keys or tokens in your source code. Secrets in Git have a special talent for becoming everyone’s secrets.


2. Understand Authentication vs Authorization

Authentication and authorization sound similar, but they solve different problems.

Authentication means: who are you?

Authorization means: what are you allowed to do?

A user being logged in does not mean they should be able to access everything. This is how you get bugs like:

1 2 /user/123/settings /user/124/settings

If changing the ID in the URL lets one user access another user’s data, the app does not have an authentication problem. It has an authorization problem.

For authentication, do not casually roll your own system. Use a trusted provider like Clerk, Auth0, Supabase Auth, Firebase Auth, AWS Cognito, or a similar service unless you have a strong reason not to.

Authentication looks simple until you remember password resets, email verification, session expiry, MFA, OAuth edge cases, account recovery, bot protection, and breach handling. That is a lot of dragons for one login form.

Also worth knowing: OAuth 2.0 is mainly for authorization. If you need user identity, you usually want OpenID Connect on top of OAuth.


3. Protect Passwords, Secrets, and Sensitive Data Properly

Security bugs often start with a simple mistake: storing sensitive data as if nobody will ever see the database.

Assume they might.

If you store user passwords, do not encrypt them. Hash them with a password-specific hashing algorithm before saving them.

1 2 3 4 const bcrypt = require('bcrypt'); const saltRounds = 10; const hashedPassword = await bcrypt.hash('user-password', saltRounds);

Plain-text passwords are an absolute no-go. General-purpose hashes like MD5 or SHA-1 are also not good enough. Use algorithms designed for passwords, like bcrypt or argon2, so leaked hashes are much harder to crack.

For data that needs to be decrypted later, such as API keys, access tokens, or sensitive user fields, use proper encryption. AES is a common standard, but the algorithm is only one part of the problem. The harder part is key management.

Environment variables are better than hardcoding secrets, but production systems should use proper secret or key management tools like AWS KMS, Google Cloud KMS, Azure Key Vault, Doppler, 1Password, or a cloud secrets manager.

The rule is simple: passwords get hashed. Secrets get protected. Keys do not get left under the doormat.


4. Validate Input on the Server

Client-side validation is great for user experience. It is not security.

Anything that comes from the client can be changed. Form fields, query parameters, request bodies, cookies, headers, uploaded files, all of it. Your frontend is not a security boundary. It is more like a polite suggestion.

Validate input on the server before using it.

For example, instead of trusting that a request body has the right shape, use a schema validation library like Zod:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { z } from 'zod'; const createUserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100), }); const result = createUserSchema.safeParse(req.body); if (!result.success) { throw new Error('Invalid input'); } const user = result.data;

Good validation protects your app from weird data, malicious data, and your own future sleepy confusion.


5. Prevent SQL Injection

SQL injection happens when user input gets treated as executable SQL.

Bad:

1 const query = `SELECT * FROM users WHERE email = '${userEmail}'`;

That looks innocent until someone sends input that changes the meaning of the query.

Good:

1 2 3 4 const user = await db.query( 'SELECT * FROM users WHERE email = ?', [userEmail] );

Parameterized queries keep user input as data, not code. Most database libraries and ORMs support this out of the box.

The rule is boring but beautiful: never concatenate user input directly into SQL queries.


6. Defend Against XSS

Cross-site scripting, or XSS, happens when attacker-controlled content gets rendered as executable code in your app.

In plain English: someone sneaks JavaScript onto your page, and now your users’ browsers are doing things they did not sign up for.

Modern frameworks like React escape values by default, which helps a lot. But you can still shoot yourself in the foot with unsafe HTML rendering, markdown previews, third-party scripts, user-generated content, and the legendary danger button itself: dangerouslySetInnerHTML.

If you need to render HTML from users, sanitize it first.

1 2 3 import DOMPurify from 'dompurify'; const safeHtml = DOMPurify.sanitize(userInputHtml);

You should also consider using a Content Security Policy. It will not magically fix unsafe rendering, but it can reduce the damage if something slips through.

Security is often about blast radius. Things will go wrong. Try to make “wrong” less exciting.


7. Protect Against CSRF

CSRF stands for cross-site request forgery. It is one of those attacks that sounds fancy, but the idea is simple.

A user is logged into your app. They visit a malicious site. That malicious site tricks their browser into sending a request to your app using their existing cookies.

The user did not mean to make the request, but their browser still has the cookies. Sneaky little goblin behaviour.

If your app uses cookies for authentication, protect state-changing actions with:

  1. SameSite cookies
  2. CSRF tokens
  3. Proper origin checks
  4. Avoiding GET requests for actions that modify data

This matters most for actions like changing email addresses, updating passwords, transferring money, deleting records, or modifying account settings.

GET should fetch. POST, PUT, PATCH, and DELETE should change things. Do not make /delete-account?id=123 a GET route unless you enjoy chaos.


8. Use HTTPS Everywhere

HTTPS should be table stakes. It protects data in transit and helps prevent man-in-the-middle attacks.

Most modern platforms make this easy. Vercel, Netlify, Cloudflare, and many managed hosting providers can issue and renew certificates automatically. Let’s Encrypt also makes certificates free if you are managing things yourself.

But do not stop at “the certificate exists.”

Make sure:

  1. HTTP redirects to HTTPS
  2. Cookies use the Secure flag
  3. Sensitive cookies use HttpOnly
  4. Your app does not load insecure mixed content
  5. Production webhooks and callbacks use HTTPS URLs

HTTPS is not glamorous, but neither are seatbelts. Still worth using.


9. Log and Monitor Security-Relevant Events

Logs are not just for debugging broken features. They are also how you notice when something weird is happening.

Track events like:

  1. Failed login attempts
  2. Password reset requests
  3. Email or password changes
  4. Permission changes
  5. Suspicious API usage
  6. Admin actions
  7. Spikes in 401, 403, or 500 responses

Tools like Datadog, CloudWatch, Loggly, Sentry, and other monitoring platforms can help you spot patterns before they become incidents.

Just be careful what you log. Do not log passwords, access tokens, API keys, session cookies, or sensitive personal data.

A log full of secrets is not observability. It is a breach with timestamps.


10. Keep Dependencies Updated

A lot of security vulnerabilities come from code you did not write.

That is not a reason to avoid dependencies. It is a reason to maintain them.

Use tools like:

  1. npm audit
  2. Dependabot
  3. Snyk
  4. GitHub Advanced Security
  5. Secret scanning
  6. Lockfile review in pull requests

For Node.js projects, you can start with:

1 npm audit

Even better, configure automated dependency pull requests so updates become part of your normal workflow instead of a terrifying annual event.

The goal is not to update everything blindly. The goal is to know when a dependency has a real vulnerability and respond before it becomes your problem.


Final Thoughts

Security does not need to be dramatic. Most of it is not about wearing a hoodie in a dark room while reading assembly code.

It is about consistent habits.

Validate input. Check authorization. Hash passwords. Protect secrets. Use HTTPS. Keep dependencies fresh. Watch your logs. Avoid building authentication from scratch unless you really know what you are signing up for.

Security is fun because it turns you into the person who sees the trapdoor before everyone else walks over it.

And honestly, that is a pretty good feeling.

Keep reading


© 2026 Adam Hultman