Keep It Simple: Real Code Examples from a Production SaaS

architectureprinciplessimplicity
Keep It Simple: Real Code Examples from a Production SaaS

The principle that pays for itself

After more than a decade writing software, the engineering principle I defend the most is brutally short: if a solution is hard to explain, it is probably wrong. Simple systems are easier to debug, cheaper to maintain, faster to onboard onto, and more resilient under pressure.

This is not an abstract idea. I want to show you what it looks like in practice, with real code from a SaaS platform I built -- a Node.js backend with Express and a Next.js frontend serving thousands of requests daily.

Example 1: Error handling without frameworks

Error handling is one of those areas where developers love to reach for complex patterns -- custom exception hierarchies, middleware chains, error mappers. Here is the entire error class that powers the backend:

export class ApiError extends Error {
  statusCode: number;
 
  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
  }
}

That is it. Seven lines. Every route in the API uses this to signal problems, and a single middleware catches them all:

export const errorHandler = (
  error: Error | ApiError | ZodApiError,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (error instanceof ApiError) {
    res.status(error.statusCode).json({
      statusCode: error.statusCode,
      message: error.message,
    });
    return;
  }
 
  // Generic fallback
  res.status(500).json({
    statusCode: 500,
    message: error.message,
  });
};

No error codes enum, no error registry, no mapping layer. When a new developer joins, they read this file once and they understand every error in the system. When something breaks at 2 AM, the error message tells you exactly what happened and where.

Example 2: Configuration as plain functions

I have seen configuration systems with YAML parsers, validation layers, environment cascade logic, and adapter patterns. Here is ours:

export function isDev(): boolean {
  return process.env.NODE_ENV === "development";
}
 
export function isProd(): boolean {
  return process.env.NODE_ENV === "production";
}

Two functions. Every module that needs to branch by environment calls isDev() or isProd(). There is nothing to mock in tests, nothing to debug, nothing to configure. The cache service uses it to decide between in-memory and Redis:

private constructor() {
  this.cache = isDev() ? new NodeCache() : null;
  this.redisClient = !isDev() ? Redis.fromEnv() : null;
}

In-memory for development so you do not need Redis running locally. Redis in production for performance. The decision is visible in a single line. No configuration file, no strategy pattern, no provider abstraction.

Example 3: Security as a list

Vulnerability scanners constantly probe production APIs with requests for /wp-admin, .php files, and .env paths. The instinct might be to reach for a WAF rules engine or a complex request analysis pipeline. Here is what actually works:

const BLOCKED_EXTENSIONS = [
  ".php", ".env", ".sql", ".bak", ".ini",
  ".log", ".save", ".old", ".backup",
];
 
const BLOCKED_PATH_PREFIXES = [
  "/wp-admin", "/wp-login", "/wp-content",
  "/phpmyadmin", "/xmlrpc", "/drupal",
];
 
export const blockSuspiciousRequests = (
  req: Request, res: Response, next: NextFunction
): void => {
  const path = req.path.toLowerCase();
 
  const blocked =
    BLOCKED_EXTENSIONS.some((ext) => path.endsWith(ext)) ||
    BLOCKED_PATH_PREFIXES.some((p) => path.startsWith(p));
 
  if (blocked) {
    res.status(403).send("Forbidden");
    return;
  }
 
  next();
};

Two arrays and a middleware. Adding a new blocked pattern is adding a string to a list. Any developer can understand this in seconds. It complements Cloudflare's WAF as a defense-in-depth layer and has blocked thousands of scanning attempts with zero false positives.

Example 4: A utility that prevents bugs with one line

One of my favorite functions in the entire codebase is this:

export function assureExists<T>(
  value: T | undefined,
  message: string
): T {
  if (value === undefined || value === null || value === "") {
    throw new Error(message);
  }
  return value;
}

It replaces scattered null checks throughout the codebase with a single, descriptive call. Instead of if (!user) throw ... repeated in dozens of places with slightly different messages and slightly different checks (some forgetting empty strings, some forgetting null), every critical lookup becomes:

const business = assureExists(
  await db.findById(businessId),
  "Business not found"
);
// business is guaranteed non-null from here

TypeScript narrows the type automatically. The error message is always descriptive. The check is always complete. Five lines replacing hundreds of inconsistent null checks.

Example 5: A frontend logger that knows when to grow

The frontend logger is intentionally the simplest possible implementation:

export const logger: Logger = {
  debug: (message: string, ...args: unknown[]) => {
    if (!isLoggingEnabled()) return;
    console.log(...formatLog("debug", message, ...args));
  },
  info: (message: string, ...args: unknown[]) => {
    if (!isLoggingEnabled()) return;
    console.info(...formatLog("info", message, ...args));
  },
  error: (message: string, ...args: unknown[]) => {
    // Always log errors, even in production
    console.error(...formatLog("error", message, ...args));
  },
};

It is a thin wrapper around console. But every logging call in the app goes through this interface. The day we need structured logging, log levels, or remote transport, we change this one file and every call site works unchanged. The key insight: we did not build the abstraction before we needed it. We built the interface so we could add the abstraction later with zero refactoring.

Why this matters for the business

Simplicity is not a developer preference -- it is a business advantage:

  • Onboarding speed. A new developer can read ApiError.ts, config/index.ts, and blockSuspiciousRequests.ts in under ten minutes and understand the core patterns of the entire backend.
  • Debugging time. When production breaks, simple code means fewer layers to dig through. The error handler is one file. The config is two functions. The security middleware is two arrays.
  • Maintenance cost. Every abstraction you add is an abstraction you maintain. A strategy pattern for environment detection means tests for the strategy, documentation for the strategy, and onboarding for the strategy. Two functions mean nothing to maintain.
  • Fewer bugs. assureExists eliminated an entire class of null-check inconsistencies. The security middleware has no conditional logic beyond array lookups, which means no edge cases.

The takeaway

Before adding an abstraction, ask: can I explain this to a teammate in one sentence? If not, you are probably solving a problem you do not have yet. Build the simplest thing that works, design it so it can grow, and wait until reality tells you it is time to grow it. In my experience, that moment comes far less often than we think.

About me

Written by Fran Llantada โ€” full-stack developer at Nieve Consulting. In my spare time I built Cliencer, a complete SaaS from scratch on my own. These articles are the engineering lessons I picked up along the way.