Mark Mwangi logoMark
Mwangi
Return to home
Get in
touch
Admin
login
SecurityApril 2, 20249 min read

Building secure apps with clarity

A practical look at writing software that stays maintainable, safe, and easy to evolve — threat modeling, explicit errors, and defense in depth.

By Mark Mwangi
Building secure apps with clarity cover coverImage

Security is not a feature you bolt on at the end; it is a property of how clearly you think about your system. In this article I walk through repeatable patterns that keep software maintainable and safe: knowing your assets, treating errors as an API, enforcing typed contracts at boundaries, and layering defenses so one mistake doesn't become a breach.

1. Start with the assets

Before writing code, map what you are protecting:

  • **Data** — user records, payment info, secrets.
  • **Credentials** — passwords, tokens, API keys.
  • **Availability** — can the service keep working under load or attack?

Tracing how each asset moves through the system shows you exactly where validation, rate-limiting, and authorization must live. Security work then follows the data instead of being sprinkled everywhere.

2. Treat errors as an API

Errors are a contract between your system and its callers. Make them intentional:

  • **Internal errors** are logged with context (request id, user id, the failure cause).
  • **User-facing messages** are generic — never leak stack traces or SQL.
  • **Observability** ties the two together so you can debug without exposing internals.
ts
// Bad: leaks internals to the client
res.status(500).json({ error: dbError.stack });

// Good: generic message, detailed log
logger.error("order.create.failed", { orderId, cause: dbError });
res.status(500).json({ error: "Something went wrong. Please try again." });

3. Enforce typed contracts at boundaries

The edges of your system — HTTP handlers, message queues, third-party calls — are where bad data enters. Validate there, once, with a schema:

ts
import { z } from "zod";

const CreateUser = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

app.post("/users", (req, res) => {
  const parsed = CreateUser.safeParse(req.body);
  if (!parsed.success) return res.status(400).json(parsed.error);
  // safe to trust req.body here
});

Doing this in one place means the rest of the code can assume valid input.

4. Defense in depth

No single control is enough. Stack complementary ones:

  • **Validation** at the boundary (Zod/schema).
  • **Authorization** checks centralized in middleware, not scattered in handlers.
  • **Rate limiting** to blunt abuse.
  • **Monitoring** to notice anomalies early.

If validation has a hole, auth still catches it. If auth misconfigures, rate limiting slows the blast. Each layer shrinks the blast radius of the others.

5. Deployment pitfalls to avoid

  • Don't ship internal error details to clients.
  • Don't commit secrets — use environment variables and a secrets manager.
  • Don't disable security headers to "make it work."
  • Don't skip dependency updates; known CVEs are the easiest attacks.

Checklist

  • [ ] Assets identified and traced through the system
  • [ ] Errors separated into logged vs user-facing
  • [ ] Boundaries validated with a schema
  • [ ] Auth checks centralized
  • [ ] Rate limiting + monitoring in place

Secure software is mostly clear software. When the boundaries, contracts, and failure modes are explicit, security becomes the natural result rather than an afterthought.

SecurityArchitectureBest Practices
Go back to all blogs →Go to all projects →Contact →

Related Articles

  • Learning by shipping small products
  • Designing for subtle performance gains
  • Practical caching strategies

About

Mark Mwangi

Creating secure and meaningful digital experiences.

Software developer focused on secure, responsive, and high-quality digital experiences.

Socials

GitHubLinkedInXE-mailinstagramfacebookwhatsappMedium

© 2026 Mark Mwangi | All rights reserved

Terms of Service•Privacy Policy

Last updated: April 30, 2026 16:23:51 UTC