
TypeScript on the frontend was never a debate — same language as the backend and infrastructure. Next.js won on ecosystem maturity and Server Components. Nx + pnpm workspaces manage 4 apps and shared packages from one monorepo. The tradeoffs are real, but the alternative — duplicated code and version drift — is worse.
The frontend architecture of TCTF is shaped by the same principle that shapes the entire stack: TypeScript everywhere. The same language that runs in our Lambda functions and defines our CDK infrastructure also powers our React components, our build scripts, and our test suites. This is not a coincidence — it is a deliberate architectural decision that compounds in value as the project grows. On top of TypeScript, we chose Next.js as the React framework, Nx + pnpm workspaces for monorepo management, and a shared package architecture that lets four applications share components, types, utilities, and an API client without duplication. This article covers the decisions, the tradeoffs, and the lessons learned from shipping four frontend applications from one repository.
TypeScript on the frontend was never a debate. It was the default from day one, and the reasons are the same as the reasons for TypeScript on the backend: one language for the entire stack means zero context switching.
A developer reading a Lambda handler can immediately understand the React component that calls it because both are written in TypeScript. The request types in the handler match the request types in the API client. The response types in the handler match the response types in the component. The error types thrown by the handler match the error types caught by the component. There is no translation layer, no impedance mismatch, no mental model switch.
This is the compounding advantage of TypeScript-everywhere. Each additional layer that uses TypeScript increases the value of every other layer. When the backend, the infrastructure, and the frontend all share a type system, changes propagate through the entire stack with compiler assistance. A field rename in the OpenAPI spec generates new TypeScript types that break the Lambda handler and the React component simultaneously — both are fixed in the same pull request, and the integration is guaranteed to work.
The alternative — TypeScript on the frontend and Python or Java on the backend — creates a boundary where type safety stops. The frontend has types, the backend has types, but the boundary between them is untyped. That boundary is where integration bugs live. TypeScript-everywhere eliminates the boundary entirely.
The practical benefit for the team is mobility. Any developer can work on any part of the stack without learning a new language. A frontend developer can review a Lambda handler PR. A backend developer can fix a React component bug. The shared language makes the team more flexible and reduces the bottleneck of specialized knowledge.
🔗TypeScript on the frontend was never a debate. Same language as the backend, same language as the infrastructure. A developer can read a Lambda handler, a React component, and a CDK stack without switching mental models.

We evaluated three React frameworks: Next.js, Remix, and Gatsby. Each has strengths, but Next.js won on three criteria: ecosystem maturity, the App Router with Server Components, and the deployment story.
Ecosystem maturity matters for a project of this scale. Next.js has the largest community, the most third-party integrations, and the most production deployments of any React framework. When we encounter an issue, the answer is usually a search away. When we need a library, it usually has Next.js-specific documentation. The ecosystem reduces the number of problems we need to solve from scratch.
The App Router with Server Components is the architectural feature that tipped the decision. Server Components render on the server and send HTML to the client — no JavaScript bundle for components that do not need interactivity. For a content-heavy platform like TCTF, this reduces the client bundle size significantly. Pages load faster because the browser downloads less JavaScript. The combination of Server Components for static content and Client Components for interactive elements gives us the best of both worlds — fast initial loads and rich interactivity where it matters.
The deployment story sealed the deal. Next.js deploys seamlessly to Netlify with automatic SSR support, edge functions, and image optimization. The build output is optimized for the deployment target, and the platform handles scaling, caching, and CDN distribution. We do not manage servers for the frontend — the deployment platform handles everything.
Remix was a strong contender with its focus on web standards and progressive enhancement. Gatsby was eliminated early — its static site generation model does not fit a platform with dynamic, authenticated content. Next.js was the framework that matched our requirements across all dimensions.
⚛️Next.js won on ecosystem maturity, Server Components for reduced bundle size, and seamless deployment. Server Components for static content, Client Components for interactivity — the best of both worlds.
For a project with four frontend applications and multiple shared packages, a monorepo is not optional — it is essential. The alternative is separate repositories for each application, which means duplicated shared code, version drift between packages, and integration testing across repository boundaries. We chose Nx for build orchestration and pnpm workspaces for package management.
Nx provides the intelligence that makes a large monorepo manageable. The dependency graph tracks which packages depend on which other packages. When a shared component changes, Nx knows which applications need to be rebuilt and tested. The affected command runs only the tasks that are impacted by the current changes, which keeps CI times reasonable even as the repository grows.
Nx also provides computation caching. If a package has not changed since the last build, Nx skips the build and uses the cached output. This applies to builds, tests, and linting. In a monorepo with dozens of packages, caching reduces build times from minutes to seconds for unchanged packages.
pnpm workspaces handle package management with strict dependency isolation. Unlike npm or yarn workspaces, pnpm uses a content-addressable store and symlinks to ensure that each package sees only its declared dependencies. A package cannot accidentally import a dependency that it does not declare in its package.json. This strictness catches dependency issues at development time rather than in production.
The combination of Nx and pnpm gives us the best of both worlds: intelligent build orchestration that scales with the repository size, and strict dependency management that prevents the dependency chaos that plagues large monorepos.
🏗️ Nx provides the dependency graph, affected detection, and computation caching. pnpm provides strict dependency isolation. Together, they make a large monorepo manageable instead of chaotic.

The monorepo contains four applications and multiple shared packages. The applications are the public portal (blog, documentation, and marketing pages), the community platform (profiles, projects, messaging, and social features), the admin dashboard (internal management and analytics), and the helpdesk (support ticketing and knowledge base). The shared packages include data synchronization utilities, internationalization with translation management, and the shared API client that provides typed methods for every backend endpoint.
The shared packages are the connective tissue of the monorepo. When a developer adds a new API endpoint, the shared API client package is regenerated from the OpenAPI spec, and both tctf-main and tctf-social-network immediately have access to the new typed method. When a translation is added to shared-i18n, both applications can use it without any coordination. When a data utility is improved in shared-data, both applications benefit.
Changes to shared packages automatically trigger rebuilds of dependent applications. Nx's dependency graph ensures that nothing ships with stale dependencies. If shared-i18n changes, both tctf-main and tctf-social-network are rebuilt and tested. If shared API client changes, every application that imports it is verified against the new types. The dependency graph is the safety net that prevents version drift.
The package boundaries are enforced by Nx's module boundary rules. An application cannot import directly from another application's source code — it must go through a shared package. This prevents the tight coupling that turns a monorepo into a monolith. Each application can be built, tested, and deployed independently, even though they share code through well-defined package interfaces.
The practical workflow for a developer is straightforward. They work in one application or one shared package at a time. Nx handles the build orchestration, dependency tracking, and cache management. The developer focuses on the code, and the tooling ensures that changes propagate correctly through the dependency graph.
📦Four apps — the public portal, the community platform, the admin dashboard, and the helpdesk — plus shared packages, all in one repo. Changes to shared packages automatically trigger rebuilds of dependent apps. The dependency graph prevents version drift.
Monorepos are not free. The benefits of code sharing, consistent tooling, and atomic changes come with real costs that grow as the repository grows.
Build times increase with repository size. Even with Nx's affected detection and computation caching, a change to a widely-used shared package can trigger rebuilds of multiple applications. CI pipelines need to be smart about what to build — running all tests for all applications on every PR would take over an hour. We mitigate this with affected-based testing, but the CI configuration is more complex than it would be for a single-application repository.
Dependency conflicts between applications require careful management. If tctf-main needs version 4 of a library and tctf-social-network needs version 5, pnpm's strict isolation helps but does not eliminate the problem. Shared packages must be compatible with all applications that use them, which sometimes means upgrading all applications simultaneously or maintaining backward compatibility in shared code.
IDE performance degrades with large repositories. TypeScript's language server needs to process more files, autocomplete takes longer, and file search returns more results. We mitigate this with project references and incremental compilation, but the IDE experience is noticeably slower than it would be in a smaller repository.
The learning curve for Nx is non-trivial. New developers need to understand the dependency graph, the affected command, the caching system, and the module boundary rules. This is additional tooling knowledge that is specific to the monorepo setup and does not transfer to other projects.
Despite these costs, the alternative is worse. Separate repositories mean duplicated shared code that drifts over time. Integration testing across repositories is harder than integration testing within a monorepo. Version management for shared packages adds overhead that Nx's dependency graph eliminates. The monorepo tradeoffs are real, but they are the better set of tradeoffs for a project of this scale.
⚖️Monorepo costs are real: build times, dependency conflicts, IDE performance, tooling complexity. But the alternative — duplicated code, version drift, cross-repo integration testing — is worse at this scale.
The frontend architecture is TypeScript everywhere, Next.js for the framework, Nx + pnpm for the monorepo, and shared packages for code reuse. Every decision reinforces the others. TypeScript connects the frontend to the backend through shared types. Next.js provides the rendering model that balances performance and interactivity. The monorepo enables code sharing without version drift. The tradeoffs are real — build times, dependency management, IDE performance — but they are the right tradeoffs for a platform that ships four applications from one repository.
Never miss a post
Subscribe to get the latest TCTF articles delivered to your inbox.