Over the past several months, I built a personal reference suite of over 50 full-stack web applications spanning more than 20 categories of software development. It started as a way to document and formalize architectural patterns in isolated environments where each concept could be examined on its own terms.
It turned into a system, complete with its own infrastructure, engineering guidelines, and accompanying audio guides. And the process of designing it reinforced my understanding of software architecture in a way that working in any single codebase probably never could, because the whole point was to build the same patterns multiple times, in multiple contexts, until every architectural decision felt deliberate rather than inherited.
The motivation
Working across multiple codebases means constantly encountering conventions that exist in the code and get followed because that's what's established, but the reasoning behind those decisions isn't always obvious. The patterns are there, they work, but the specific tradeoffs that led to them aren't something that comes up during a sprint. Why does middleware ordering matter in exactly this sequence? What actually happens when business logic leaks from a service layer into a controller? How should a centralized error handler propagate exceptions from the database layer through the API response and into a frontend error display without scattering try/catch blocks across every controller?
These are the kind of deeper architectural questions that only surface when there's space to slow down and examine a pattern in isolation, outside of a production context where the priority is shipping features. This project was about creating that space.
Web-based AI platforms (and hitting their limits)
The earliest apps in the collection were built with the assistance of web-based AI development platforms, starting with Claude's web interface.
Every conversation on these platforms has a limited context window, and when building a full-stack application with a frontend, backend, database layer, and configuration files, that window can seem to close fast. I'd be in the middle of debugging a middleware chain and the conversation would lose track of how the routes were structured three prompts ago.
My workaround was to front-load every prompt with massive amounts of context. My prompts grew into multi-page documents. Every time I wanted to modify a single file, I'd paste in the current state of related files, the project structure, the conventions I wanted followed, and explicit instructions about what not to change. The need to improve that process led me to begin writing formal development guidelines.
Those guidelines evolved into a comprehensive set of coding conventions and architectural rules that I refined through hundreds of iterations. Standardized API response contracts with consistent success/error shapes. Centralized error handling architecture with error code mapping between backend and frontend. Class-based service layers with dependency injection, singleton patterns, and strict controller/service/repository separation. Rules for Express middleware ordering, React component modularization, and variable naming consistency from database columns and document schemas through backend logic and frontend state.
The document grew to cover 19 sections of engineering conventions, and the process of writing it turned out to be just as valuable as the apps themselves, because formalizing a rule about how a centralized error handler should propagate errors across the stack requires actually tracing that path from database exception to API response to frontend error display.
Finding the right tools
I worked with several AI-assisted development tools along the way. GitHub Copilot in VS Code and Cursor both offer codebase-aware features, and both were genuinely useful for inline completions and quick edits. But for the kind of work I was doing, building full applications from scratch against a detailed set of architectural guidelines, I eventually landed on Claude Code as my primary tool. The combination of direct filesystem access, far more efficient use of the context window, and the ability to hold cross-cutting architectural reasoning in a single session made a noticeable difference when working through multi-layered builds where routes, services, database schemas, and frontend state all needed to stay in sync.
The shift from web-based AI platforms to a CLI-based tool with full filesystem access was where everything changed. The context limitations that had defined my entire workflow essentially disappeared. This kind of locally-integrated CLI tool reads, writes, searches, and navigates the codebase directly, without relying on embedding-based retrieval to surface relevant files. The constant overhead of pasting context and micromanaging edits across files was gone. I could describe intent at a higher level and trust that, as long as my guidelines were thorough, the implementation would be solid.
However, I continued to run into context limits early on, because my workflow still needed refining. I was trying to draft entire execution plans for new applications within the conversation, outlining every file, every route, every service method, and every database table inline before writing a single line of code. For complex apps, those conversations would grow so large that even the automatic context compaction would fail. On several occasions the request threw a flat "prompt is too long" error, and every attempt to continue hit a dead end. The only option was to manually summarize the current state and start a new chat from scratch.
That obstacle turned into another catalyst for improvement. It pushed me toward drafting complete implementation plans as separate documents, independent of the chat context entirely. That solved the problem and turned out to be a much better workflow regardless, because having a standalone plan document that both I and the CLI tool could reference at any point during a build made the whole process more organized and predictable.
Having full codebase access also made it more straightforward to implement comprehensive testing suites. With complete visibility across the controller, the routes, the service layer, and the database schema all at once, integration tests produced in this environment more accurately reflected how the application works as a whole rather than testing functions in isolation.
The guidelines I'd written out of necessity for the web platform became even more powerful in this context. A short CLAUDE.md configuration file at the root of the project references a much more comprehensive document of coding conventions and architectural rules, and every session starts with that shared understanding of how code should be written. No more pasting and no more re-explaining. The conventions were always right there, enforced from the first line.
And the tooling is still evolving. MCP servers now represent an entirely new dimension of what's possible, extending the development environment with access to external tools and services well beyond the local filesystem.
Why repetition matters
Building the same architectural patterns multiple times, in different contexts, is a very effective approach to solidify engineering principles and architectural best practices.
Layered architecture is a good example. I built a data parsing application three separate times. The first version used JavaScript with a purely functional approach: plain functions for routes, controllers, and service logic, with no use of classes anywhere. The second version rebuilt the same app using JavaScript classes with private fields, dependency injection, and strict separation between layers. The third version rebuilt it again with a class-based backend in TypeScript, where interfaces, generics, and type-only imports reinforced the architecture with compile-time contracts across every layer.
After completing the third version, layered architecture had gone from an abstract concept to a set of specific decisions I'd made and remade about file organization, dependency flow, and responsibility boundaries. I gained a clearer understanding of the difference between a well-separated service layer and one where business logic had leaked into the controller, because I'd built it both ways and seen what happens to testability and maintainability when the boundaries blur.
The same principle applied across the whole collection. I built two JWT authentication apps with different token storage strategies (httpOnly cookies vs. in-memory), stripped of every other piece of application logic, to make the token lifecycle and refresh mechanics completely visible.
I built three RAG applications, each using a different API (OpenAI, Anthropic, Google) against the same retrieval pipeline and vector store, which made it immediately clear which parts of a RAG architecture are universal and which are provider-specific.
I built an event-driven dashboard using Server-Sent Events to understand real-time server push patterns without the complexity of a full WebSocket implementation; and a background job processing system with BullMQ and Redis to examine how worker processes, retry strategies, and queue management interact in a production-like environment.
The portal as a system design exercise
Somewhere along the way, what had started as a collection of independent apps became a system. Managing several dozen applications requires infrastructure, and designing that infrastructure turned into its own exercise in system design.
Every app runs on a structured port allocation scheme with backends and frontends in separate ranges, the frontend port always derived from the backend port by a fixed offset. Apps are organized into numbered categories (Data Management, Node Frameworks, Agentic Tooling and so on), and each category gets a ten-port block. The whole system is managed by pm2 with a single ecosystem configuration file.
I built a portal dashboard as a zero-dependency HTTP server that serves as the single source of truth for the entire collection, documenting every app, its stack, its database dependencies, its process names, and its configuration files.
Designing this system exercises exactly the kind of thinking that comes up in system design contexts: how to organize resources, avoid conflicts, maintain a single source of truth, and build something operable by anyone who might not currently have all required context within clear sight.
Audio guides via the OpenAI API
I also built an automated pipeline that uses the OpenAI API to generate structured audio companion guides for each application in the collection.
Each guide is scripted in segments and then converted to audio files using OpenAI's text-to-speech API. The scripts walk through every file in an application's codebase, breaking down what each piece of code is doing, how the different layers connect to each other, and why the architecture is structured the way it is.
The audio walkthroughs became another useful tool for keeping application context sharp, because the process of scripting each guide was its own exercise in articulating architectural decisions clearly.
The guidelines as a product
The development guidelines deserve their own section, because they ended up being one of the most valuable outputs of the entire project.
What started as a survival mechanism for working within Claude's context limitations became a comprehensive engineering reference. The document covers naming conventions that maintain consistency across the full stack (how variable names transform from database columns through backend logic to frontend state), architectural patterns (centralized error handling with error code mapping, controller/service/repository separation, standardized API response contracts), security practices (parameterized queries, Helmet configuration, CORS policies, JWT refresh token rotation), and scalability principles (modular directory organization by feature, single source of truth for configuration, graceful server shutdown patterns).
These aren't theoretical preferences. Every rule exists because I hit a real problem during development and needed a consistent solution. The standardized API response format exists because inconsistent response shapes between apps made frontend error handling unnecessarily complex. The centralized error handler architecture exists because scattering try/catch blocks across controllers made debugging across the stack nearly impossible. The single source of truth principle for environment configuration exists because the same value defined differently in two places produced bugs that were unreasonably difficult to trace.
Every application I built during this process follows these same patterns. Any app in the collection has the same directory structure, the same controller conventions, the same error response format, the same approach to frontend-backend communication. That consistency across more than 50 applications is, in a lot of ways, the real product of this whole effort.
What this sharpened
Building several dozen applications in isolation didn't produce a list of facts. It sharpened the way I think about software architecture as a discipline rather than a set of tools. The specific technologies matter less than the patterns: separation of concerns, single responsibility, consistent interfaces, predictable error handling, scalable and modular application design, single source of truth, and clear boundaries between layers.
The repeated process of building application after application made the reasoning behind conventions concrete in a way that's hard to get from documentation alone. Middleware ordering reveals its importance when error handling sits before routes and nothing catches. Service layers prove their value when controller functions grow to 200 lines and become impossible to test in isolation. Modular directory organization matters when onboarding back into an app built three months ago and the decisions made regarding file structure aren't immediately apparent.
Software development is a field where regular practice matters. There's so much to know across the full stack that without consistent engagement with these patterns, day-to-day familiarity with specific details and approaches is likely to fade. This collection of applications is, more than anything, a system for keeping all of that sharp: a controlled environment for examining architectural decisions, a reference for the conventions that hold a codebase together, and a living document that grows as new patterns become relevant.
New apps get added as new topics come up, the project structure evolves as priorities shift, and the guidelines get updated as better approaches surface. Maintaining it is part of the practice.
The workflow I've built around AI-assisted development has become just as much a part of the system as the applications themselves: comprehensive implementation plans drafted before a single file is written, engineering guidelines enforced automatically from the start of every session, and an iterative process for planning, building, and testing that gets more refined with every new project.
If you're interested in reading an earlier chapter of this story, I wrote about my journey in software development here.
Top comments (0)