From Hype & Vibe to Product Reality
Architecting a Dashboard Rewrite from Next.js to React and Vite. A deep dive into modern dashboard engineering reality, evaluating tradeoffs, and course correction.
Last Updated: 2026-03-01 | Tags: Engineering, Architecture, React, Frontend
It's a perfect degree Celsius lovely evening, and I am writing this in a room while enjoying the pleasure of a lush green sight and the coldness of the thin fresh breeze of a high forest. The first implementation was fully handwritten based on real-life experience. And yeah, AI helped clean the grammar and summarize it later, cause why not?
A year into my software engineering career, I found myself in the middle of a major B2B platform rewrite. It was a business-critical application with complex domain rules, multi-region support, multiple payment processors, payment configurations, strict access control, tight security, and multi-tenancy, all under a team working with heavy delivery pressure.
I am writing this as a point of reflection for my future self, and to share what I learned about architecture, tradeoffs, leadership, AI-assisted delivery, and engineering quality while still being early in my career. Product and organization details are intentionally anonymized.
Application Scope
For context, this was a complex B2B platform built to serve business owners and employees managing fast daily operations: catalog items, orders, service areas, payment devices, receipts, financial systems with banks and payouts, real-time business intelligence charts, taxes, invoicing, legal documents, and AI integration on the horizon.
Some aspects:
- Complex B2B Platform: Centralized platform for managing critical business operations.
- Multi-Region: Multiple operating regions with different product and compliance needs.
- Multi-Payment Provider: Integrations across acquiring, payouts, and platform payments.
- Multi-Payment Types: POS, online, mobile app, and third-party commerce integrations.
- Transaction Methods: Local and international cards, digital wallets, and cash.
- Complex Access Control: Role-based permissions with multi-tenancy and feature whitelisting per business unit.
- Real-Time Management: Orders, catalogs, devices, shifts, and frequently updating business intelligence dashboards.
- AI Agent Integration: For managing manual user workflows, under development.
History - The Three Acts of Application Evolution
Act I: The Legacy Foundation (v1)
Before June 2025
The original application was built during the early days of the organization. Bootstrapped with create-react-app and powered by Redux for both client and server state management, with React Router, MUI, and Jest, it reflected the common React stack choices of that era. It had also evolved through version upgrades over time, including React Router updates and a later CRA-to-Vite migration.
As the first implementation, it served a simpler set of business requirements: single-region support and a single payment-provider scope. It helped the company move fast, prototype quickly, and support a growing client base.
Over time, as the organization scaled, the application had to support the multi-region, multi-provider platform described above. But it had to do that on top of a foundation that was never designed for that level of complexity. Legacy MUI with runtime CSS generation spoke for its time, and the Redux-heavy patterns did not age well compared with newer frontend tooling. The application eventually suffered from large bundle sizes, performance bottlenecks, and years of compounded technical debt from rapid delivery cycles.
By 2025, it was time to invest in a healthy revamp.
Act II: The Next.js Misalignment (v2)
Timeline: June 2025 - October 2025
During this period, more frontend applications in the organization were moving toward Next.js. For this B2B platform, the adoption of Next.js was driven more by organizational momentum and framework hype than by a strict alignment with product requirements. Next.js had become the default standard, so it became the framework of choice for the revamp without enough visible requirement analysis, implication review, or tradeoff discussion.
The squad leading the initial revamp handed the project over to my squad of four developers. At that stage, the application was around 85% dev-complete for a scope covering one major payment provider, roughly 30% of the entire platform scope. Our task was to finish the remaining 15% and deploy the new platform to production alongside the legacy application.
Onboarding onto the project surfaced immediate DX friction. I had to get a POSIX dev environment running by installing Ubuntu 20 LTS on WSL2 because I could not get the project working properly on Windows. I kept hitting a Next.js cookie error, later debugged to a node sub-dependency of a package that broke due to Windows file naming conventions. The trigger was painfully small: my project folder path contained whitespace in the name of a parent folder. WSL2 did not solve the problem either, because the project somehow triggered sustained 600 MB/s+ file reading operations on my NVMe drive with no end. That was the first frustration before I even saw the application up and running.
Every developer dealt with their own set of problems, not just development-wise but also UX-wise. I believe poor DX often directly transfers into poor UX, and we experienced a lot of that.
Where the Architecture Drifted
The core issue was not simply "Next.js is bad." That would be too easy, and not true. The issue was the default-driven choice.
Despite the ability to build a CSR or hybrid application with Next.js, the project defaulted to a server-heavy model. The framework's conventions became louder than the product's actual needs. This was a fully authenticated B2B operations platform consuming existing REST APIs through a BFF. There was no meaningful SEO surface. The product needed fast client-side interactions, predictable auth handling, direct API visibility, and stable tenant context.
The misalignment got worse when weak architectural decisions were accelerated by AI-heavy implementation. AI tools were very effective at scaffolding Next.js, shadcn, and Tailwind components quickly. But without strong human guidance, the focus shifted from "implement what is correct" to "build what works." AI solutions worked for the smaller context an LLM could hold, not for the larger context the application needed to survive. This was the Claude Sonnet 3.5 era, and AI tools were not as capable as the front-runners are today.
What the implementation inherited:
- Framework over product: The application was a fully authenticated platform consuming REST APIs, with no meaningful SEO requirement, but still moved toward a server-heavy architecture because that had become the organizational default.
- Server-first model for client-heavy work: Simple user flows carried server-rendering concerns they did not naturally need.
- Obscured debugging: Network calls that were once visible in browser DevTools now had to be traced through server context and CloudWatch logs.
- Fragmented state: Cookies, server state, client UI state, loading states, and hydration states all participated in flows that should have been simpler.
- Middleware bottleneck: Auth, redirects, tenant context, and feature access gathered in a single sensitive decision point.
- Extra API hop: Browser requests passed through the Next.js server layer before reaching the BFF.
- Inconsistent hydration: Server-rendered responses still needed client hydration, background refetching, and client state recovery.
- Generated over architected: AI acceleration produced rapid scaffolding, but repeated prop drilling, derived states, duplicated logic, loose typing, and deeply scattered feature logic kept coming back.
When the final phase reached production, the concerns stopped being theoretical.
The production reality:
- Environment-specific bugs: Some issues appeared in production but could not be reproduced locally or in the similarly deployed testing environments.
- Unstable loading: Route transitions and data readiness did not line up cleanly.
- Inconsistent tenancy state: Separate parts of the application could hold different slices of tenant-related data at different times.
- High cognitive overhead: Data could be fetched via server actions, API routes, server components, client components, direct hooks, or custom data layers.
- Team morale: Developers were defending and patching a system they did not originally architect. The technical debt became emotional debt for the team that inherited it.
Act III: The Final Destination (v3)
November 2025 - moving strong to the future
The Point of No Return
Complaining was not a strategy. The delivery had been rushed, the planning had not been deep enough, and the result was an over-engineered product where maintainability was suffering. I still do not think that was purely an individual-dev failure. It was a planning, architecture, and incentive failure too.
With the team, seniors, product owners, and leads all recognizing the friction, I advocated strongly for an architectural reset. My message was roughly this: if we wanted the future development flow to be straightforward and wanted to eliminate the performance, debugging, and production-confidence issues we were seeing, we had two serious options. Either we had to change the Next.js architecture heavily enough to remove the server-heavy assumptions, or we had to rebuild the frontend boundary cleanly with plain React again.
That opened the door. Leadership gave the green light to assemble the senior engineering group and define a path forward.
The Pre-Bootstrap
The resulting deep-dive sessions allowed us to evaluate the application against the company's tech standards and the product's real needs. The decision, backed by senior engineers and approved by the leads, was to rebuild the frontend boundary cleanly.
I had strong opinions and a lot of influence on that direction, but the final choices stayed with the leads. Senior engineers backed the reasoning, lead engineers kept oversight.
I was assigned to handle:
- Architecture research, stack evaluation, and long-term patterns.
- Bootstrapping the new application foundation.
- Code ownership and quality guardrails.
Why Not a Hybrid Next.js Approach?
The first responsible question was not "React or Next.js?" It was: can we keep the existing Next.js codebase and remove only the parts that are hurting us?
Technically, yes. Next.js can support client-heavy and hybrid approaches. But that was not the debate. The question was whether that path would solve our product problem with less risk than rebuilding the frontend boundary cleanly.
The answer was no.
Moving pages toward client-side rendering would have removed some SSR waiting time, but it would not have removed the shape of the codebase we inherited. The application was already deeply coupled to App Router conventions, server components, route handlers, middleware, cookies, server-side auth assumptions, framework-specific loading behavior, and AI-generated code built around those assumptions.
Converting the project would have been a rewrite disguised as a refactor. We would keep the framework around which most of the wrong assumptions had already been built, spend weeks stripping features away, and still carry the mental model that had already failed the product.
Starting fresh with Vite and React was not about chasing a new trend. It was about making the application honest again: a client application consuming APIs, with one clear runtime model.
The Business Case for a Rewrite
Rewrites are dangerous. They can destroy hidden business behavior, delay delivery, and create a second system that looks cleaner but understands less. So the decision had to be weighed against real options:
- Continue the Next.js implementation: Fastest on paper, but already failing confidence in production.
- Modernize the legacy app: Safer in terms of known behavior, but still carrying years of Redux, MUI, bundle, and architectural debt.
- Rebuild the frontend boundary cleanly: Highest short-term discipline required, but the cleanest path to a maintainable product.
Because the API boundary was stable, the domain was understood, and the scope could be copied feature-by-feature instead of rediscovered from zero, a clean rebuild was estimated at 3-4 months. The case was not "new code is better." The case was that a controlled rebuild was cheaper than years of compounding patchwork on a misaligned architecture.
Pragmatic Technology Selection
The stack had to survive team review, senior oversight, lead approval, and the product reality.
| Constraint | Reality | What it meant |
|---|---|---|
| Team size | 5 developers with mixed frontend, fullstack, and mobile backgrounds | Patterns had to be simple enough to teach, review, and repeat. |
| Timeline | Around 12-16 weeks for a production-ready replacement path | No experimental architecture that required months of team maturity. |
| APIs | Existing backend and BFF contracts had to be consumed as they were | The frontend needed schemas, adapters, and predictable error handling. |
| Product shape | Fully authenticated internal B2B platform with no meaningful SEO requirement | SSR was not a default benefit, fast client-side operation mattered more. |
| Organization | The company was already invested in React | The right move was to simplify inside the React ecosystem, not introduce a foreign platform. |
| Scale vs. velocity | Micro-frontends and pure FSD added too much governance for the squad size | A balanced feature-based architecture gave enough boundaries without slowing everyone down. |
| AI-assisted delivery | AI-generated code still needed strong human review and correction | The structure had to make the correct path obvious for both humans and AI tools. |
The Selected Stack I proposed
| Category | Technology | Why it fit this product |
|---|---|---|
| Framework | React 19 | Stable React foundation and ecosystem depth without forcing server rendering into a product that did not need it. |
| Build Tool | Vite 7 | Fast dev server, fast HMR, simple static build output, and a runtime model that matched the product. |
| Routing | TanStack Router | Type-safe routing, file-based routing, and validated search params for dashboards, filters, date ranges, pagination, and tenant context. |
| Server State | TanStack Query | Built for API-driven server state: caching, background refetching, stale data handling, invalidation, and devtools. |
| Client State | Zustand | Small mental model for true client-only state such as UI preferences, temporary selections, and persisted local choices. |
| UI | Shadcn + Tailwind CSS 4 | Component ownership, build-time styling, design flexibility, and fast iteration without carrying the old UI stack forward. |
| Validation | Zod | Runtime validation with TypeScript inference at route, form, and API adapter boundaries. |
| Testing | Vitest + Playwright | Fast unit feedback and browser-level E2E coverage for business flows that could not be trusted by unit tests alone. |
Execution & Ownership
Bootstrapping the Foundation
A clean stack does not prevent a messy codebase. Without strict boundaries, even Vite and React can degrade into another chaotic folder tree. My first job was to turn the rewrite from a concept into an engineering system.
- Pragmatic architecture: I evaluated Feature-Sliced Design, micro-frontends, Bulletproof React, and similar patterns. Pure FSD was too heavy for our squad size. Micro-frontends made no sense for five developers owning one product surface. So I adapted a balanced feature-based architecture: strong enough to enforce boundaries, simple enough for rapid onboarding.
- Eliminating guesswork: I bootstrapped the application shell with auth boundaries, strict routing, API access patterns, schemas, environment handling, proprietary packages, devtools, linting, formatting, and CI quality checks. When a developer started a feature, the answers to "where does this live?" and "how do I fetch this?" were already standardized.
- Knowledge transfer: I conducted KT sessions with real examples, not just library introductions. I compared the new patterns against the old Redux/MUI patterns and the failed Next.js implementation, because the team needed to understand not only what to use, but why the previous choices hurt us.
Quality Guardrails
As a code owner, my development-to-review ratio shifted heavily. The squad still used AI for acceleration, so I was reviewing four to five PRs daily, often spanning 1,500-2,500 lines of code.
AI is fast, but it does not understand system ownership by itself. I spent 2-6 hours a day catching prop drilling, derived state anti-patterns, loose typing, naming drift, duplicated logic, and business logic bleeding into UI components. Leaving 20-30 comments on a PR was not about ego. It was about protecting the baseline.
Every accepted hack today becomes tomorrow's standard.
Real-World Gains - What Actually Landed
These are internal before/after observations from our application and development workflow, not a public benchmark and not a framework shootout. The exact numbers should be read with the environment, data size, branch, internal build/test setup, network conditions, and traffic profile in mind.
Performance
| Metric | Before | After |
|---|---|---|
| Initial Load | ~5-7 seconds in internal testing of the heavier features | Under 2 seconds for the same features |
| Infrastructure & Cost | Amplify hosting with SSR/server execution cost, SSR request duration cost, CloudWatch log cost, and extra proxy traffic through the Next.js server | Static Amplify asset hosting only |
| Bundle Size | Heavier bundle profile from accumulated legacy/framework weight such as MUI runtime styling and Redux | ~30% smaller in measured internal build, improved by lazy loading and better feature-level tree shaking |
| API Visibility | Browser requests proxied through Next.js server layer | Direct browser-to-BFF requests visible through browser DevTools |
| Test Execution | Jest baseline | Up to 20x faster with Vitest, plus added E2E automation with Playwright |
Developer Productivity
- Restored browser DevTools debugging: API calls, payloads, failures, headers, and timing became visible again in the browser.
- Fast HMR with Vite: No heavy page-level server/render loop on the local dev server.
- One client-side mental model: Pages, filters, forms, and dashboards followed a predictable browser-first lifecycle.
- Consolidated devtools: TanStack Query Devtools, TanStack Router Devtools, Zustand Devtools, and React Scan made invisible state transitions and render loops easier to inspect.
- Clear feature boundaries: A feature had a clear home, a clear export boundary, and a standard way to access APIs, schemas, hooks, constants, and types.
Architecture That Could Scale With the Team
Before
The previous structure made the framework louder than the product. One feature could live across routes, actions, data folders, API routes, nested components, stores, hooks, and shared utilities. Before understanding the business flow, a developer first had to understand where the implementation had scattered the pieces.
src/
├── app/
│ ├── (route-group-name)/
│ │ └── (route-sub-group-name)/
│ │ └── feature-name/
│ │ ├── [id]/
│ │ │ ├── crud-name/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ └── feature-name/
│ │ ├── route.ts
│ │ └── other-route.ts
│ └── layout.tsx
├── actions/
│ └── feature-name/
│ └── feature-action-name/
│ ├── feature-action.actions.ts
│ └── other-action.actions.ts
├── data/
│ └── feature-name/
│ └── feature-data-name/
│ ├── feature.ts
│ └── other-data.ts
├── features/
│ └── feature-name/
│ ├── components/
│ │ ├── another-nested-folder/
│ │ │ └── another-nested-folder/
│ │ │ ├── some-random-utils-folder/
│ │ │ └── .../
│ │ ├── another-nested-folder/
│ │ └── another-nested-folder/
│ ├── stores/
│ ├── hooks/
│ ├── types/
│ └── .../
├── node_modules/
│ ├── proprietary-packages/
│ └── .../
├── components/
├── constants/
├── types/
├── utils/
└── .../
After
src/
├── routes/
│ ├── _layout/
│ │ └── feature-name/
│ │ ├── [id]/
│ │ │ └── index.ts
│ │ └── index.tsx
│ ├── __root.tsx
│ └── _layout.tsx
├── features/
│ └── feature-name/
│ ├── api/
│ ├── components/
│ │ ├── [feature-name]-x.tsx
│ │ ├── [feature-name]-y.tsx
│ │ └── [feature-name]-z.tsx
│ ├── constants/
│ ├── hooks/
│ ├── pages/
│ ├── schemas/
│ ├── types/
│ ├── utils/
│ └── index.ts
├── node_modules/
│ ├── proprietary-packages/
│ └── .../
├── libs/
├── components/
├── configs/
├── stores/
├── types/
└── .../
Why it mattered:
- A feature finally had one predictable home.
- Routes stayed routes.
- Feature directories owned their API calls, schemas, hooks, constants, types, and UI.
- Shared directories stopped being dumping grounds.
- PR reviews became less about detective work and more about correctness, edge cases, naming conventions, and maintainability.
The Middleware Bottleneck
The old implementation placed too many decisions in middleware. Auth, redirects, tenant context, and feature access all depended on one sensitive request-time decision point.
This is not actual production code. It represents the shape of the problem.
export const middleware = async (request: NextRequest) => {
const session = await getSession(request)
const pathname = request.nextUrl.pathname
if (!session) {
if (isPublicRoute(pathname)) return NextResponse.next()
if (isAuthRoute(pathname)) return NextResponse.next()
return NextResponse.redirect(new URL('/login', request.url))
}
if (isAuthRoute(pathname)) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
if (!hasRequiredContext(session)) {
return NextResponse.redirect(new URL('/select-context', request.url))
}
if (!canAccessRoute(session, pathname)) {
return NextResponse.redirect(new URL('/not-authorized', request.url))
}
return NextResponse.next()
}
The new model moved route-level access checks closer to the feature that needed them.
export const Route = createFileRoute('/feature-path')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
}
})
Access logic moved closer to the feature. One route could change without shaking the whole application request path.
Centralized Feature Access Control
Feature availability used to appear in pages, route logic, navigation, and validation. That looks manageable in one place, then quietly duplicates in another, then becomes fragile without looking obviously broken.
// /src/app/(route-group-name)/(route-sub-group-name)/feature-name/page.tsx
type PageProps = {
searchParams: Record<string, string | undefined>;
};
const Page = async ({ searchParams }: PageProps) => {
const session = await requireSession();
const featureContext = await getFeatureContext(session);
const data = await getFeatureData({
contextId: featureContext.id,
filters: searchParams,
});
const isAvailable = featureAvailabilityCriteria({
conditionA: hasFeatureFlag(session, 'feature-flag-a'),
conditionB: hasPermission(session, 'feature:read'),
conditionC: isContextReady(featureContext),
});
if (!isAvailable) {
redirect('/not-available');
}
return <Component data={data} />;
};
export default Page;
The new direction was a centralized, typed feature-scope model.
// /src/constants/common.constants.ts
export const featureScope: FeatureScope[] = [
{
name: 'Feature name',
url: '/feature-path',
metadata: {
group: 'feature-group',
priority: 1
},
scopeCriteria: {
requiredFlags: ['feature-flag-a'],
requiredPermissions: ['feature:read'],
requiredContextStates: ['ready']
}
}
]
Why it mattered:
- Feature access became a whitelist model instead of scattered conditional checks.
- Navigation, route guards, and feature-level validation could read from the same source of truth.
- TanStack route types helped keep URLs from turning into random strings.
- Introducing a new feature became a controlled configuration change.
This is one of the places where human architecture mattered more than generated code. AI could produce the component. It did not produce this system-level thinking by itself.
API Call Lifecycle
The old request path added a frontend server hop before reaching the existing BFF.
+---------+ cookies for auth/server state +---------------+ token auth +-----+
| browser | -----------------------------------> | Next.js server | --------------> | BFF |
+---------+ +---------------+ +-----+
^ |
| |
| server-modified cookies/data |
+---------------------------------------------------+
The new request path was more direct.
+---------+ auth +-----+
| browser | -----------------------------------> | BFF |
+---------+ +-----+
^ |
| |
+----------------------------------------------+
Why it mattered:
- The browser regained direct visibility into API requests through DevTools.
- The request path became shorter and easier to reason about.
- Auth stayed handled at the existing BFF boundary instead of being split across frontend server logic and browser logic.
- For our workload, most dashboard requests still needed fresh business data from the BFF. The frontend server layer added another place to route, log, and debug those requests without giving enough product value back.
State Management Simplified
The old state management model had too many possible owners. Depending on the feature, state could start in a server component, move through a server action or route handler, get reflected through cookies, hydrate on the client, and then be handled again by hooks or a local store.
+-------------------+ +----------------+ +----------------+
| Server components | | Route handlers | | Server actions |
| initial data/load | | API proxy/load | | mutation/load |
+-------------------+ +----------------+ +----------------+
| | |
v v v
+---------------------------------------------------------+
| Cookies / auth context / tenant context / search params |
+---------------------------------------------------------+
|
v
+-------------------+ +----------------+ +----------------+
| Client components | | Client hooks | | Zustand store |
| hydration state | | fetch/load | | mixed UI/data |
+-------------------+ +----------------+ +----------------+
The new model separated server state from client UI state.
+-----------------------------------------------+
| TanStack Query |
| API data, caching, loading, invalidation |
+-----------------------------------------------+
+-----------------------------------------------+
| Zustand |
| Only required client UI state |
+-----------------------------------------------+
Why it mattered:
- TanStack Query became the owner of API state, caching, loading, invalidation, and refetching.
- Zustand was reserved only for necessary client UI state.
- Developers no longer had to guess whether a loading state belonged to a server action, client action, form action, route transition, or local store.
- The data lifecycle became visible, predictable, and easier to test.
Again, dead simple. That was exactly the value.
AI Impact & Modern Workflows
The AI era changed developer mentality very fast. Somewhere along the way, "make it correct" started turning into "make it work." Around the environment I was in, speed became so loud that people started acting like technical debt had magically disappeared.
That was one line I did not want the team to cross. So I pushed one principle again and again:
Never commit code you do not fully understand, regardless of whether it works.
AI usage had to be directed toward specific, high-value work:
- Planning narrow implementation options before a developer made the final call.
- Converting known old patterns into approved new patterns, such as MUI to Shadcn or Redux-shaped server state to TanStack Query.
- Repeating an already-established feature pattern by referencing a similar completed feature.
- Explaining complex concepts.
- Migrating specific validation patterns, such as Yup to Zod.
- Structuring documentation.
- Speeding up repetitive conversion work where the pattern was already clear.
What I did not support was prompt-engineering entire features and accepting the output as it is, because it looked complete. We moved toward a simple cycle: Plan, Implement, Review. AI could accelerate the work, but it could not replace ownership. The developer still had to understand the decision, the code, the tradeoff, and the failure modes.
Tradeoffs
No architecture decision came for free. The rewrite was the right decision, but it still had tradeoffs that had to be accepted consciously.
| Tradeoff | Decision | Why it was acceptable |
|---|---|---|
| Scalability vs. velocity | Chose a simpler feature-based structure over micro-frontends | The team had a tight timeline, mixed experience levels, and one product shell. |
| Familiarity vs. modernity | Introduced TanStack Router, TanStack Query, Zustand, and Vitest | The learning curve paid back through better DX, testing, and performance. |
| Ecosystem gravity | Stayed with React instead of moving to Vue, Svelte, or Solid | The organization already had React knowledge and shared frontend investment. |
| Ideal backend vs. reality | Accepted constraints from existing BFF contracts | The rewrite scope was frontend-first, so backend changes had to stay limited. |
| AI velocity vs. ownership | Used AI for acceleration and code generation, but kept strict human review boundaries | Speed only helped when the team still understood the code. |
Lessons for the Future
- Evaluate, do not follow: Popularity is not architecture. A framework should be selected because it fits the product, not because the community has decided it is the default answer.
- Simple can still be heavy: "Batteries included" sounds convenient until those batteries become concepts the product does not need. Hidden complexity is still complexity.
- AI is an accelerator, not the owner: AI can help write code faster, explain ideas faster, and migrate patterns faster. But architecture still needs human judgment, product context, and accountability.
- Developer experience becomes product quality: Slow builds, unclear state, scattered feature logic, and painful debugging do not stay as internal problems. They eventually become user-facing quality problems.
- Leadership is not always a title: Sometimes leadership is being the person who says the uncomfortable technical thing at the right time and then takes responsibility for proving the better path.
Where I Am Now
At one point, the manager himself said something close to:
"Wow, this application is good. It's fast and everything. A lot better than what we had."
And I did enjoy that. Pure developer dopamine. Not because it was praise for the sake of praise, but because it meant the product felt better. The rewrite was not just technically cleaner on paper. Someone using and judging the product could feel the difference.
I do not care to be the loudest "100x engineer" in the room. I want to be the engineer who deeply understands the product, weighs the tradeoffs, solves the problem, and delivers a system with care that the team can actually work in.
This journey from a legacy React app, through a framework misalignment, and finally to a modern React + Vite + TanStack architecture became a masterclass in product-first engineering. Identifying the need for a reset, pitching the architecture, bootstrapping the foundation, and protecting the quality standard as an engineer relatively early in my career became a massive milestone.
It reinforced a core truth: choosing the right technology is not about picking the most popular framework. It is about understanding the product deeply enough to know what the product does not need.
Written in March 2026.