MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Local Text-to-Speech is finally practical on CPU-only machines

2026-01-23 12:15:57

Until recently, getting natural-sounding text-to-speech usually meant using a hosted service. If you wanted good quality, you ended up calling an API from Amazon, Microsoft, or Google. That works, but it means relying on a remote service and paying per use for a task that doesn't inherently need to be remote.

There are now models that run fast enough on a regular CPU to be useful in practice. They don't need a GPU, and the audio quality is comparable to what you get from common cloud TTS services. Everything runs locally, without relying on third-party APIs.This aligns well with the expectations of privacy-oriented users.

I wanted to make local, CPU-only text-to-speech conversion usable without requiring people to understand or assemble the underlying tooling, so I built a simple GUI converter that can convert long texts to speech in a matter of minutes.

It supports basic desktop workflows like drag and drop, which makes it more convenient than uploading text to a service and downloading the generated audio files. You can drop in text files, run batch conversions, and get audio files out, all locally.

This is what the main conversion tab looks like while a conversion is running. The focus is on keeping the workflow simple and local: drop text files, process them in batches, and get audio files out without external services.

Here's what the main conversion tab looks like while a conversion is running.

The application is free and runs entirely offline. Project details and downloads are available at https://jimlet.com

This project exists because CPU-only text-to-speech is finally fast enough to be useful. That makes it practical to build local tools that don't rely on cloud APIs or specialized hardware, and to keep them simple and self-contained.

Complexity Can't Be Eliminated. It Can Only Be Moved.

2026-01-23 12:09:33

Cover Image Photo by Sunder Muthukumaran on Unsplash

Six patterns that determine where complexity lives in your codebase — and how to choose consciously

A senior engineer made our API faster by caching responses. Query time dropped 80%. We celebrated.

Two months later, the cache was stale. Data was wrong. Users complained. We spent weeks debugging cache invalidation.

The speed didn't come from nowhere. The complexity didn't disappear. We just moved it.

This pattern behaves like a conservation law from physics. Not perfectly, but close enough to be useful.

Why Complexity Relocates (Not Disappears)

In physics, certain quantities can't be created or destroyed. Only transformed or moved. Energy conservation says energy can't be created or destroyed, only converted (chemical to kinetic, kinetic to heat). Momentum conservation says total momentum stays constant in a closed system. Mass conservation says mass doesn't appear or disappear, just rearranges.

These aren't guidelines. They're laws. You can't violate them. You can only work within them.

Software has something similar: essential complexity (the inherent difficulty your problem requires) can only move, not disappear. Larry Tesler famously called it "Conservation of Complexity": complexity can't be eliminated, only moved. UX designers know Tesler's Law intimately. But while this principle is well-recognized in design circles, software architects rarely discuss it explicitly or apply it systematically.

I've noticed we treat "simplification" as if we're eliminating complexity rather than relocating it. We don't measure both sides of the trade. We don't name what's actually being relocated.

This isn't quite like physics conservation laws, where total energy stays exactly constant. Software complexity can increase or decrease. But there's a pattern, and a floor.

Every problem has essential complexity, what Fred Brooks called the inherent difficulty of what you're trying to solve. Authentication must verify identity. Distributed systems must coordinate. These requirements create complexity that can only relocate, or be eliminated by dropping features entirely. You can't design it away.

Then there's accidental complexity, from how we implement solutions. Poor abstractions, unnecessary indirection, tech debt. This can be eliminated through better design.

When net complexity increases (code drops 40%, config grows 60%, net +20%), you're seeing accidental complexity added during relocation. When complexity genuinely disappears (deleting 500 lines of dead code), you're removing accidental complexity that never contributed to solving the problem.

The pattern: essential complexity moves. Accidental complexity varies. And there's a floor: you can't simplify below essential complexity without losing functionality.

To be precise: when we say "complexity relocates," we mean essential complexity (the irreducible difficulty of your problem domain). You can't simplify a tax calculation system below the complexity of the tax code itself. You can only choose where that essential complexity lives in your architecture.

This explains why some systems resist simplification. You're not fighting bad design. You're hitting essential complexity. The question shifts: Where should this essential complexity live to minimize total cost?

When you "simplify" a system, you're not eliminating complexity. You're relocating it. When you make a decision configurable instead of hardcoded, you haven't reduced the number of decisions. You've moved where the decision happens. When you cache data, you haven't eliminated the work of keeping data fresh. You've transformed query complexity into cache invalidation complexity.

Understanding relocation patterns changes how you think about software design. You stop asking "how do I eliminate this complexity?" and start asking "where do I want this complexity to live?"

Six patterns emerge consistently. We'll call them relocation patterns that behave like conservation laws. Not physics-perfect, but strong enough to guide architectural decisions.

The Six Patterns

Pattern 1: Complexity Relocation

The caching story is a perfect example. Before caching, we had high query complexity: every request hit the database, queries were slow, load was high. Cache management complexity was zero because we didn't have a cache. After caching, query complexity dropped dramatically. Requests were fast, database load was low. But cache management complexity exploded. We now had staleness issues, invalidation logic, consistency problems, memory pressure.

Total complexity didn't decrease. We moved it from "slow queries" to "cache management." The system felt simpler in one dimension and more complex in another. The essential complexity of data consistency didn't disappear. It moved from query time to cache invalidation. But if your cache implementation is inefficient, you've added accidental complexity on top.

I've learned you can't eliminate complexity. You can only move it. The question isn't "how do I make this simpler?" The question is "where should this complexity live?"

Consider adding an abstraction layer. Before abstraction, you have high duplication complexity: the same database query logic appears in twenty places. But you have low abstraction complexity because there's no layer to understand. After creating an ORM, duplication complexity drops to near zero. Database logic lives in one place. But abstraction complexity rises. Now you need to understand the ORM, its query builder, its caching behavior, its transaction handling.

You didn't reduce total complexity. You traded duplication complexity for abstraction complexity. The essential complexity of database operations remains. You just centralized where it lives. Whether abstraction adds accidental complexity depends on design quality.

Whether that's a good trade? Depends on your context. For a system with many developers, centralizing complexity in an abstraction that a few people deeply understand might be better than distributing complexity across the codebase where everyone encounters it. For a tiny system with two developers, the abstraction might not be worth it: the duplication is manageable, the abstraction is overhead.

This is why "simplification" is such a loaded term. When someone says "let's simplify this," what they usually mean is "let's move complexity from where it bothers me to somewhere else." (Which, to be fair, is sometimes exactly what you want.) But recognize you're relocating complexity, not eliminating it.

Where can complexity go? You can push it to infrastructure: move complexity from application code to Kubernetes, but now you need to understand Kubernetes. You can push it to configuration: move complexity from code to config files, but now configuration management becomes complex. You can push it to runtime: use dynamic dispatch instead of explicit wiring, but behavior becomes harder to trace. You can push it to operations: microservices simplify individual services but operational complexity explodes.

The complexity goes somewhere. It doesn't vanish. Choose consciously where you want it to hurt least.

Pattern 2: Knowledge Relocation

Knowledge can't be reduced, only relocated. You can't reduce what needs to be known about a system. You can only change where that knowledge lives.

Take abstraction layers again: before adding an ORM, knowledge about database queries is distributed across every function that touches the database. After adding an ORM, that knowledge concentrates in the ORM layer. Total knowledge hasn't decreased. You still need to understand how queries work, how connections are managed, how errors are handled. You've just relocated the knowledge.

This creates a trade-off. Distributed knowledge means each piece is simple: local context is enough to understand what's happening. But finding patterns is hard because knowledge is scattered. Global understanding requires synthesizing information from many places.

Concentrated knowledge means finding answers is easy: look in the abstraction layer. But each piece is more complex: the ORM is harder to understand than any individual query was. Which distribution is better depends on your team, your system, your change patterns.

When a new developer asks where logic lives, I can say "check the ORM" instead of "check twenty controllers." Same knowledge needed, better location. But now that developer needs to understand the ORM's complexity.

I've seen teams struggle with this trade-off. A microservices architecture distributes knowledge across service boundaries. Each service is simpler to understand in isolation, but understanding cross-service workflows requires mental synthesis of multiple codebases. A monolith centralizes that knowledge. You can trace a request end-to-end in one codebase, but the concentration makes the monolith harder to navigate.

The knowledge exists either way. The question is: where does it hurt least? If you have autonomous teams, distributing knowledge across service boundaries might work. If you have frequent cross-cutting changes, centralizing knowledge in a monolith might be better. You're not reducing knowledge. You're choosing where developers encounter it.

Pattern 3: Decision Relocation

Decisions can't be eliminated. Every decision must be made somewhere. Moving where decisions happen doesn't reduce total decisions.

Consider configuration. You have a decision: "Which database connection string to use?" You can make it in code: if environment equals production, use this connection; otherwise use that one. Or you can make it in config: read from environment variable or config file. Same decision. Different location. Someone still decides what the database URL is. The decision moved from code to configuration. It didn't disappear.

The choice of where to make decisions has consequences. Compile-time decisions mean fast runtime but slow development: changing behavior requires changing code. Runtime decisions mean slow runtime but fast iteration: change config and restart. Configuration-time decisions mean flexible behavior but configuration becomes complex: now you have configuration management, templating, validation. Convention-based decisions mean simple explicit code but you must learn the conventions. "Magic" behavior that's invisible until you know the pattern.

I've debugged systems where configuration grew so complex it became code by another name. YAML files with conditionals, includes, variable substitution. Essentially a programming language without the tooling. The decisions didn't decrease; they just moved to a less maintainable place.

The reverse is also true. Hard-coding decisions in code means every environment difference requires a code change. I've seen teams with many if-statements checking environment variables because they never moved decisions to configuration. Same total decisions, worse location.

Feature flags are the modern version of this trade-off. You move decisions from deploy time (merge to production) to runtime (toggle in a dashboard). This gives you safety and speed. You can deploy dark and enable gradually. But you pay in testing complexity: with N flags, you have 2^N possible system states. Three flags mean eight configurations to test. Ten flags mean 1,024. The decision didn't disappear. It multiplied.

Pick where decisions happen based on who needs to change them and how often. If operators need to change behavior without deploying code, configuration makes sense. If developers need to understand decision logic during debugging, code makes sense. If the decision rarely changes, hard-coding might be fine. You're not reducing decisions. You're choosing who makes them and when.

Pattern 4: Failure Mode Transformation

Failure modes can't be eliminated. They can only be transformed. You can't eliminate how systems fail. You can only trade failure modes you understand for failure modes you don't.

Moving from synchronous to asynchronous is classic. Synchronous systems fail with timeouts, deadlocks, resource exhaustion when threads block. Asynchronous systems fail with message loss when queues drop messages, ordering issues when messages arrive out of sequence, partial failures when some operations complete and others don't. You traded known failures for different failures. Total failure surface area might even increase.

I've debugged async message loss that took days to track down. With sync systems, timeouts show up immediately in logs. I'm not saying one is better. I'm saying they fail differently, and you're choosing which failure mode you'd rather debug.

The same pattern appears everywhere. Move from monolith to microservices? You trade in-process call failures (immediate stack traces) for network call failures (distributed tracing, timeouts, partial failures). Move from SQL to NoSQL? You trade constraint violations (database enforces referential integrity) for data inconsistency (application must enforce integrity).

I've watched teams adopt new technologies expecting them to be "more reliable," then spend months learning their failure modes. The new system wasn't less reliable. It just failed differently. And the team's existing monitoring, debugging practices, and mental models were all tuned to the old failure modes.

This doesn't mean you shouldn't go async, or adopt microservices, or use NoSQL. It means recognize the trade-off. You're not eliminating failure modes: you're choosing which failure modes you'd rather handle. Maybe async failures are easier to handle in your context. Maybe you have better tools for debugging message loss than deadlocks. Maybe your team has experience with distributed systems failure modes. That's a valid trade. Just don't pretend the old failure modes disappeared: they transformed into new ones. And plan to invest in learning how the new system fails.

Pattern 5: Testing Burden Relocation

Testing burden can't be reduced, only relocated. You can't reduce what needs to be tested. You can only move where testing happens.

Type systems are the clearest example. Without static types, you need more runtime tests because type verification happens at runtime: tests must verify both types and logic. With static types, you need fewer runtime tests because type verification happens at compile time: tests verify logic only, types are checked by the compiler.

Testing effort didn't disappear. It moved from runtime tests to compile-time checks. The shift has trade-offs. Compile-time verification gives faster feedback: you know about type errors before running code. But it adds compilation overhead and can't test runtime-only behaviors like "does this API actually return the structure we expect?" Runtime testing gives slower feedback but tests actual system behavior. Same amount of verification work. Different timing.

The same pattern appears with integration vs. unit tests. Heavy integration testing means you verify actual system behavior but tests are slow and brittle. Heavy unit testing with mocks means tests are fast and isolated but you need integration tests anyway to verify the mocks match reality. The testing burden didn't change. You're choosing between "test real interactions slowly" and "test mock interactions quickly plus verify mocks match."

I've seen teams swing between extremes. All integration tests: comprehensive but painfully slow, so developers avoid running them. All unit tests with mocks: fast but brittle when mocks drift from reality, leading to "tests pass but production fails." The burden exists either way.

The question is: where do you want verification to happen? Early in development (static types, unit tests, compile-time checks) or late in deployment (runtime tests, integration tests, production monitoring)? Each approach has different feedback loops and different failure modes. You're not reducing testing. You're choosing when you discover problems and how much machinery you need to discover them.

Pattern 6: Assumption Visibility Trade-off

Assumptions can't be eliminated, only made explicit or implicit. You can't reduce assumptions. You can only change their visibility.

An implicit assumption looks like this: a function expects user.email to exist and be a string. The code just calls user.email.lower() and hopes. An explicit assumption documents it: add type hints, add null checks, add validation. Same assumption: user must have an email that's a string. Now it's visible instead of hidden.

Implicit assumptions are cheaper to write but expensive to debug. When they're violated, you get cryptic errors: AttributeError: 'NoneType' has no attribute 'lower'. You have to trace back to figure out the assumption. Explicit assumptions are expensive to write but cheap to debug. When they're violated, you get clear errors: ValueError: User must have email. Total cost is conserved. You're choosing when to pay it: upfront with explicit checks, or later when debugging implicit assumptions.

The same trade-off appears with API contracts. Implicit contracts mean less documentation, less validation code, faster development. But when clients violate expectations, you get runtime failures that are hard to diagnose. Explicit contracts mean more upfront work (OpenAPI specs, request validation, comprehensive error messages) but violations are caught immediately with clear feedback.

I've debugged production issues that took hours to diagnose because assumptions were buried deep in code. "Why does this fail for some users but not others?" Eventually you discover an implicit assumption: the code assumes users have an email, but imported users from legacy systems don't. The assumption existed either way. It just wasn't visible until it broke.

The question is: where do you want to pay the cost? Write explicit checks upfront (slower development, clearer debugging) or deal with implicit assumptions when they break (faster development, cryptic failures)? Neither reduces the total assumptions in your system. You're choosing whether to document them in code or discover them during debugging.

Why These Patterns Matter

Once I understood these relocation patterns, how I approached design changed completely. When someone proposes "simplifying" the system, the first question should be: "Where does the complexity go?" It doesn't disappear. It moves. The proposal might still be good: maybe the new location is better. But recognize it's a trade, not an elimination.

This doesn't mean simplification is impossible. You can absolutely reduce total complexity:

Delete dead code: If code contributes nothing to requirements (truly dead), removing it eliminates complexity. No relocation.

Use better abstractions: Replace 50 lines of manual logic with 1-line library call. The library maintains complexity, but amortized across thousands of users, your system's complexity drops.

Remove accidental complexity: Decouple unnecessarily entangled components. Clean up tech debt. Simplify overly complex solutions.

The key: These eliminate accidental complexity. Essential complexity (what the problem inherently requires) is what relocates, not eliminates.

Common examples: "Let's use microservices to simplify development." Where does complexity go? From code organization to service coordination. You trade monolith complexity for distributed system complexity. "Let's add caching to speed things up." Where does complexity go? From query performance to cache management. You trade slow queries for invalidation logic. "Let's make the API more flexible." Where does complexity go? From API code to API consumers. You trade server complexity for client complexity.

These might all be good decisions. But they're trades, not improvements in absolute terms. Microservices might be the right trade if you have the team size and tooling to handle distributed systems. Caching might be right if query performance is your bottleneck and you can handle invalidation. Flexible APIs might be right if you have sophisticated clients and want to iterate server-side less often.

The key is naming what's being relocated and choosing where you want it to live. Before changing anything, identify the relocating quantity: Is this complexity? Where will it move? Is this knowledge? Where will it concentrate? Is this a decision? Where will it happen instead?

How to Work With These Patterns

Where should complexity live? Where will it hurt least?

Example: API design. You can have a complex API with simple client code, or a simple API with complex client code. Neither eliminates complexity: they distribute it differently. Complex API means server handles edge cases, versioning, validation. Clients just call simple methods. Simple API means server provides primitive operations. Clients compose them to handle edge cases.

I've worked with APIs that do everything (clients love it, server team drowns) and APIs that provide primitives (clients write boilerplate but have control). Same complexity, different distribution.

The complexity is conserved. Where should it live? If you have many clients, push complexity to the API: pay the cost once, save it N times. If you have few clients and a rapidly changing server, simple API with complex client code might work fine.

Choose your trades consciously. You can't eliminate conserved quantities. But you can choose better locations. Moving complexity from the hot path to the cold path is usually good: cache invalidation runs less often than queries. Moving complexity from novices to experts is often good: let experienced developers handle the abstraction so junior developers use a simpler interface. Moving complexity from many places to one place is often good: centralize knowledge even if that one place becomes more complex.

But measure both sides. When you move complexity, measure both the source and destination. Code complexity decreased 40%, configuration complexity increased 60%, net result is +20% total complexity. If you only measure one side, you'll think you eliminated complexity. You didn't: you relocated it, and it grew. Measure what you gained and what you paid.

Accept that some things don't simplify. If you keep trying to simplify something and complexity keeps showing up elsewhere, maybe the system has inherent complexity. Some problems are just complex. No architectural cleverness eliminates their complexity. You can only distribute it more or less well. Recognizing irreducible complexity lets you stop fighting it and start managing it.

What Actually Lasts

But step back from the code for a moment. If everything eventually gets rewritten or deleted, what's the point of these choices?

The answer: some things outlast the code. Patterns last. Design patterns outlive implementations. Separation of concerns, dependency injection, event-driven architecture: these patterns transfer across rewrites. The specific code gets replaced but the patterns persist. When you're choosing where complexity lives, you're really choosing patterns. Those patterns will outlast the code.

Understanding lasts. Understanding the domain outlives the code. How the business works, what users need, why systems interact: this knowledge compounds over time. The code gets rewritten but understanding remains. When you're deciding where knowledge should live, invest in shared understanding. Documentation rots but team knowledge grows.

Tests as specification last. Tests document expected behavior. They outlive implementations. When you rewrite, tests preserve requirements while code changes. The investment in test quality pays off when refactoring or replacing code. Tests preserve intent: what should this system do?

Team culture lasts. How your team writes, reviews, and maintains code outlasts any particular codebase. Quality standards, review practices, testing discipline: these transfer to the next system. When you're working with these relocation patterns, you're building patterns of thinking that persist beyond the current code. Invest in culture. It compounds.

The liberation comes from seeing these patterns. Once you understand that complexity relocates rather than disappears, you stop looking for solutions that eliminate it. You look for solutions that put complexity where it belongs. You measure both sides of the trade. You name what's being relocated and choose where it lives. And you invest in what actually lasts: patterns, understanding, and culture. While accepting that code is temporary.

These relocation patterns aren't limitations. They're reality. You can't violate them. But you can work with them. And working with them is better than pretending they don't exist.

Note: Originally published on ITNEXT: https://itnext.io/complexity-cant-be-eliminated-it-can-only-be-moved-d122f7952715

Upgrading to Serverless Framework v4: The Why and the How

2026-01-23 12:08:21

Self-Introduction

Hi there!

I'm Yoshihide Shiono, a freelance developer who absolutely loves TypeScript.

To be honest, I'm not exactly thrilled about upgrading from Serverless Framework v3 to v4. However, in this article, I'll explain why upgrading to v4 will eventually become a necessity for all of us.

What is the Serverless Framework?

The Serverless Framework is a Node.js-based tool designed to make developing and deploying serverless applications much easier.
It allows you to run code without having to worry about servers or clusters, so you can focus entirely on application development.

The Serverless Framework consists of an open-source CLI (the serverless command) and a web dashboard. By combining these, you can manage the complete lifecycle of your serverless applications.
With AWS Lambda, you can run code for virtually any type of application or backend service without needing any administration.

Differences between Serverless Framework v3 and v4

Official Blog: Serverless Framework V4 Generally Available

Licensing Changes

Up until v3, the Serverless Framework was free to use. However, starting with Serverless Framework v4, organizations with an annual revenue exceeding $2 million will need to purchase a paid subscription.
(Conversely, this means that if your annual revenue does not exceed $2 million, you can continue to use the Serverless Framework for free, just as before. It works similarly to Docker's model.)

Authentication required for every serverless command

Even if you don't purchase a paid subscription, authentication is required every time you run the serverless command. If you aren't authenticated, you'll be prompted to run serverless login.

This user information corresponds to an account on the web dashboard, so you'll need to register if you haven't already.
Alternatively, if an administrator already has access to the dashboard, you can use a license key issued from there to bypass the login prompt.

Focus on AWS Lambda support going forward

While previous versions supported Google Cloud, Azure, and others in addition to AWS, Serverless Framework v4 seems to be moving towards unifying development primarily around AWS.

Native TypeScript Support

serverless-esbuild, which was commonly used in v3, is no longer usable. Instead, TypeScript support is now built-in.
Currently, only esbuild is supported, but there's a possibility that other bundlers (like Webpack or Rollup) will be supported in the future.

Why we're gradually entering the era of Serverless Framework v4 (whether we like it or not)

Node.js v22 (LTS) doesn't run on Serverless Framework v3

Since Serverless Framework v4 introduces a paid model for some, there are many cases where teams would prefer to stick with v3 for as long as possible.

However, there is a shocking fact I want to shout from the rooftops:
As of March 2025, Node.js v22 does not work with Serverless Framework v3.

AWS Lambda itself announced support for Node.js v22 LTS in November 2024.

Since the EOL (End of Life) for Node.js v20 LTS is April 30, 2026, it is only a matter of time before we are forced to adopt a configuration of Serverless v4 + Node.js v22.
For this reason, at Enerbank Inc., we are also gradually proceeding with the introduction of Serverless Framework v4.

Ecosystem plugins are being absorbed into Serverless Framework v4

Not only is core development shifting to v4, but the ecosystem cultivated during the v3 era is expected to be gradually absorbed or phased out with the arrival of v4.

Let's consider the pattern of using esbuild as a TypeScript bundler.
serverless-esbuild was an external plugin in v3, but esbuild is built into the core functionality in v4. Consequently, including serverless-esbuild, serverless-webpack, or serverless-plugin-typescript will result in an error. (This is a breaking change.)

Example using esbuild in Serverless Framework v3

# serverless.yml

plugins:
  - serverless-esbuild

custom:
  esbuild:
    minify: true
    sourcemap: true

Example using esbuild in Serverless Framework v4

You can now describe esbuild settings using the build.esbuild option.

# serverless.yml

build:
  esbuild:
    bundle: true
    minify: true

You can also specify a configuration file like esbuild.config.ts.

# serverless.yml

build:
  esbuild:
    configFile: esbuild.config.ts
// esbuild.config.ts

export default () => {
  return {
    minify: true,
    sourcemap: true,
  }
}

How to run Serverless Framework v4 in GitHub Actions

Running in GitHub Actions without serverless login

You will need an access key named SERVERLESS_ACCESS_KEY.
Generate an access key in the Serverless Framework dashboard and use it.
Since this is sensitive information, it's best to register it in GitHub Actions secrets and reference it from there.

By passing the GitHub Actions secret to the environment variable in serverless.yml, execution will proceed normally.

# serverless.yml

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy App
        run: serverless deploy
        env:
          SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}

If it works locally but not in GitHub Actions

You might need NODE_OPTIONS='--openssl-legacy-provider'.

When upgrading from Node.js v16 to v18, the OpenSSL major version was upgraded from v1 to v3. Depending on the libraries you use, you may need to enable the --openssl-legacy-provider option to maintain compatibility with the older OpenSSL.

Summary

  • Starting with Serverless Framework v4, the licensing has changed; organizations with over $2 million in annual revenue must purchase a paid subscription.
  • Since Serverless Framework v3 does not support Node.js v22 (LTS), upgrading to Serverless Framework v4 is necessary.
  • Beyond core development, the plugin ecosystem is gradually being absorbed into Serverless Framework v4, which may force breaking changes in your project configuration.

Building in Public: CV Analyzer -Act 4 · Scene 1: The Analysis Component

2026-01-23 12:05:37

This post shows how to fix broken navigation in a React app by wiring a real feature page into React Router v6.4+, turning a missing route into a working product flow.

Click here for Act 3 · Scene 2

Table of Contents

  • Overview
  • What the AnalysisForm Is Really About
  • Why the AnalysisFormPage Exists: And Why I Didn’t Route the Component Directly
  • Mental Model
  • Why this scene matters

Act 4 · Scene 1: The Analysis Component

In this scene, I build and render the AnalysisFormPage, properly integrating it into the React Router route tree so the application’s main feature becomes accessible through navigation.

Overview

Auth was fixed in the last act. By the end of the previous act, authentication routes were fully wired into the application. Before that, clicking navigation links like Auth or Analyze Candidate resulted in an error boundary and the familiar message:

  • No routes matched this location.

Auth has now been resolved. But the same issue still exists for Analyze Candidate, and this scene focuses on fixing that properly.

This scene focuses on:

  • Introducing the AnalysisFormPage
  • Rendering it correctly through the router
  • Completing the navigation flow without errors

What the AnalysisForm Is Really About

This is where CVs and job descriptions enter the system.
Where raw text becomes something useful.
Where AI Talent Profile Analyzer stops being a name and starts becoming a tool.

For now, I am not after logic. I am just building the structure. Putting it together like a freaking LEGO block.

Here’s the initial JSX structure of the component:

return (
  <div className={classes["analysis-form-container"]}>
    <div className={classes.heading}>
      <h1>Analyze a Candidate</h1>
    </div>

    <div className={classes.body}>
      <form>
        <Card className={classes.cardClass}>
          <h2>Upload CV</h2>

          <div className={classes.actions}>
            <label className={classes.uploadButton}>
              Upload PDF or Text Files
              <input
                type="file"
                accept=".pdf,.txt"
                onChange={fileChangeHandler}
                hidden
              />
            </label>

            <button
              type="button"
              className={classes.secondaryButton}
              onClick={() => setShowPasteCv(true)}
            >
              Paste CV Text
            </button>

            <p>OR</p>

            <textarea
              placeholder="Paste CV text here..."
              value={cvText}
              onChange={cvTextChangeHandler}
            />
          </div>

          <div className={classes["lower-part"]}>
            <h2>Paste Job Description</h2>

            <textarea placeholder="Paste the job description here..." />

            <Button type="submit" className={classes.button3}>
              Run AI Analysis
            </Button>
          </div>
        </Card>
      </form>
    </div>
  </div>
);

The JavaScript behavior will be refined later. For now, this establishes the UI structure.

Why the AnalysisFormPage Exists: And Why I Didn’t Route the Component Directly

I don’t usually route components directly. I prefer that:

  • Pages should handle routing.
  • Components should handle UI.

So, I created AnalysisFormPage, imported the AnalysisForm component, and let it do its thing.

Code right here:

const AnalysisFormPage = () => {
  return <AnalysisForm />;
};

Then I wired it into the router.

children: [
  { path: "/", element: <HomePage /> },
  { path: "/auth", element: <AuthPage /> },
  { path: "/analysis-form", element: <AnalysisFormPage /> },
];

After saving and reloading, navigating to Analyze Candidate rendered the form successfully.

No error boundary. No warnings. Just a clean render.

See image below:

Mental Model

At this stage:

  • Routes map cleanly to pages. Yep! all pages
  • Pages handle routing
  • Components handle UI and interaction
  • Navigation reflects the actual application state

Why This Scene Matters

This scene shows that I:

  • Treat routing as application architecture, not an afterthought
  • Separate pages from components intentionally
  • Expect and resolve incomplete states during development

Thanks for reading.
Let’s move on to the Next Scene.

We in the Building…
Building in Progress…

No Laying Up Podcast: The Booth Vol. 24 | Trap Draw, Ep 379

2026-01-23 12:03:44

Neil and Cody are back, dishing on winter home woes, their latest media obsessions, and a hilariously weird grocery store adventure Neil had. They're also promoting ways to support their foundation and join their golf community for exclusive perks!

Watch on YouTube

☸️ AWS 143: Enterprise Kubernetes - Provisioning a Private Amazon EKS Cluster

2026-01-23 12:00:33

🏗️ Production-Ready K8s: Building the xfusion-eks Cluster

Hey Cloud Architects 👋

AWS EKS

Welcome to Day 43 of the #100DaysOfCloud Challenge!
Today, we are tackling one of the most powerful services in the AWS ecosystem: Amazon EKS. The Nautilus DevOps team is preparing for a new microservices deployment, and they need a cluster that is both highly available and secure. Our mission is to provision a cluster using the latest stable version (1.30) while ensuring the control plane remains completely private.

This task is part of my hands-on practice on the KodeKloud Engineer platform, which is my go-to for simulating complex infrastructure scenarios.

🎯 Objective

  • Create an IAM Role named eksClusterRole with the necessary trust relationships.
  • Provision an EKS cluster named xfusion-eks using Kubernetes version 1.30.
  • Configure Networking across three Availability Zones (a, b, and c) for High Availability.
  • Restrict access by setting the Cluster Endpoint to Private.

💡 Why Private Endpoints Matter

By default, EKS cluster endpoints are public. By switching to Private Access, the Kubernetes API server is not accessible from the internet. All communication with the cluster must originate from within your VPC or via a connected network (like a VPN or Direct Connect).

🔹 Key Concepts

  • Control Plane: The managed Kubernetes API server and etcd database that AWS handles for you.
  • IAM Cluster Role: A specific role that allows the EKS service to manage resources (like ELBs or EC2 instances) on your behalf.
  • High Availability (HA): Deploying across multiple AZs ensures that even if one data center goes down, your cluster control plane remains operational.

🛠️ Step-by-Step: Infrastructure Workflow

🔹 Phase A: Identity & Access Management (IAM)

Before creating the cluster, we must define the permissions EKS needs to operate.

  • Create Role: Navigate to IAM and create a role named eksClusterRole.
  • Trusted Entity: Select "EKS - Cluster" as the service that will use this role.
  • Policy: Attach the AmazonEKSClusterPolicy.

🔹 Phase B: Cluster Configuration

Now, we move to the EKS console to define the cluster's "brain."

  • Name: xfusion-eks.
  • Version: Select 1.30 (the latest stable version requested).
  • Service Role: Choose the eksClusterRole we created in Phase A.

🔹 Phase C: Networking & Security

This is the most critical phase for meeting the security requirements.

  • VPC: Select the Default VPC.
  • Subnets: Ensure subnets from AZs us-east-1a, us-east-1b, and us-east-1c are selected.

* Cluster Endpoint Access: Change the radio button from "Public" to Private.

🔹 Phase D: Verification

EKS clusters take about 10–15 minutes to provision.

  • Status Check: Wait until the cluster status changes from Creating to Active.
  • Configuration Audit: Click on the "Networking" tab to verify the endpoint access is indeed set to "Private" and that all three AZs are listed.

✅ Verify Success

  • Cluster Identity: The cluster is named xfusion-eks and running K8s version 1.30.
  • Role Association: The cluster successfully assumed the eksClusterRole.
  • Zero Exposure: The API server has no public URL, confirming the private configuration.

📝 Key Takeaways

  • 🚀 Version Stability: Always use the latest stable version supported by EKS for the best security patches.
  • 🛡️ Network Isolation: Private endpoints are the standard for financial and healthcare sectors to meet compliance.
  • 📦 IAM Dependencies: EKS cannot start without its service role. Always verify the trust relationship if the cluster fails to create.

🚫 Common Mistakes

  • Public Access Enabled: Forgetting to toggle the "Private" endpoint setting exposes your API server to the web.
  • Missing AZs: Selecting only one AZ removes the High Availability benefit of a managed service.
  • Role Permissions: Using a standard EC2 role instead of an EKS Service Role will cause the cluster creation to hang.

🌟 Final Thoughts

You've just provisioned a production-grade Kubernetes control plane! While the setup today was done via the console, these same parameters form the basis of automated Infrastructure as Code (IaC) using Terraform or AWS CDK.

🌟 Practice Like a Pro

Sharpen your Kubernetes skills in a real-world sandbox:
👉 KodeKloud Engineer - Practice Labs

🔗 Let’s Connect