Nx Monorepo Essentials: Crafting a Next.js 15 Frontend (Part 2/3) | AI Hub Blog | AI Hub
Tutorial
Nx Monorepo Essentials: Crafting a Next.js 15 Frontend (Part 2/3)
S
Sidharrth Mahadevan
AI Research Lead
February 22, 2025
13 min read
Image by Unsplash
Learn how to build a scalable Next.js 15 frontend inside an Nx monorepo. This comprehensive guide covers App Router setup, shared libraries, React 19 integration, and production build optimizations.
Nx Monorepo Essentials: Crafting a Next.js 15 Frontend (Part 2/3)
By Sidharrth Mahadevan | Published: February 22, 2025
Introduction
Welcome back to our Nx monorepo series! In Part 1, we established our foundational Nx workspace and explored how a monorepo architecture streamlines team collaboration and code organization. Now, we are ready to build the user-facing layer of our stack: a high-performance frontend powered by Next.js 15 and React 19.
Next.js 15 introduces major structural changes, including asynchronous lifecycle hooks (such as dynamic routing APIs), the integration of the React Compiler, and adjusted caching defaults. Managing these features in a single-app repository can sometimes become unruly as your application grows. By nesting our Next.js 15 application inside an Nx Monorepo, we gain access to modular development, local and remote caching, static dependency analysis, and seamless cross-application code sharing. This guide walks you through setting up Next.js 15 within an Nx workspace, implementing dynamic routes with the App Router, and utilizing shared React components.
Deepen Your Knowledge
Continue exploring related insights and research in Tutorial.
Get curated tutorials, tool comparisons, and industry news delivered directly to your inbox. No spam, ever.
By subscribing, you agree to our Terms of Service and Privacy Policy.
Series Overview
Part 1: Nx Monorepo Essentials: Laying the Foundations
Part 2: Nx Monorepo Essentials: Crafting a Next.js 15 Frontend (You're here!)
Part 3: Nx Monorepo Essentials: Integrating a NestJS Backend
Why Pair Next.js 15 with Nx?
Before diving into the code, let's understand why combining Next.js 15 and Nx creates an exceptionally robust development ecosystem. While Next.js provides excellent routing, rendering capabilities, and server-side features, Nx elevates your workflow by streamlining workspace orchestration and build pipelines.
Comparison: Multi-Repo vs. Nx Monorepo for Next.js
Feature
Traditional Multi-Repo Setup
Nx Monorepo Setup
Code Sharing
Private npm packages, constant publishing overhead, and version mismatches.
Direct, local imports using TypeScript path aliases (zero-publish overhead).
CI/CD Build Times
Full rebuild of apps on every change, leading to long build queues.
Incremental builds and nx affected logic that only rebuilds what changed.
Caching
Requires complex custom CI caching configurations.
Out-of-the-box local and remote computation caching via Nx Cloud.
Dependency Management
Manually synchronizing React, Next.js, and ESLint versions across multiple repositories.
Disjointed linting, testing, and formatting scripts.
Consistent tooling, generators, and workspace-wide executors.
Workspace Structure Recap
To keep our bearings, let's review our repository's directory layout as we build out the client-side system:
my-workspace/
├── apps/
│ └── client/ # Our Next.js 15 frontend application
├── libs/
│ └── ui-shared/ # Shared presentation components
├── nx.json # Monorepo execution and caching configuration
├── package.json # Single source of truth for dependencies
└── tsconfig.base.json # Base TypeScript paths mapping library aliases
Setting Up Next.js 15 in Your Nx Workspace
To integrate Next.js 15, we must install the official Nx Next.js plugin. This plugin provides code generators and executors specifically optimized to build, serve, and test Next.js applications.
Step 1: Add the Nx Next.js Plugin
Run the following command in the root of your workspace to add the plugin dependency:
nx add @nx/next
This command updates your root package.json with the required dependencies and configures global settings within nx.json.
Step 2: Generate the Next.js Client Application
Next, generate your application. We will call our app client. Run the generator tool with the following flags to customize your workspace architecture:
--directory=apps/client: Specifies the location of your code inside the apps directory.
--style=tailwind: Pre-configures Tailwind CSS v4, generating the required config files.
--appDir=true: Instructs the generator to use Next.js's modern App Router layout system instead of the legacy Pages Router.
--srcDir=true: Keeps our React pages and components neatly tucked inside a clean src/ directory.
--e2eTestRunner=playwright: Sets up Playwright for fast, headless end-to-end integration tests.
Deep Dive: Next.js 15 App Router Layout
Next.js 15 changes key mechanics of the App Router—most notably, dynamic parameters (params and searchParams) inside layout.tsx and page.tsx are now asynchronous promises. Let's review how we construct our root layout and page views to match Next.js 15's native behavior.
1. Creating the Root Layout
Our root layout defines the structural envelope of our frontend application, wrapping every page with global fonts and custom global styling. Here, we'll implement a layout using Next.js 15's dynamic metadata engine and Google's pre-rendered variable fonts.
Now, let's create a functional home page. We'll write dynamic home page markup inside page.tsx. This file displays a quick feature-grid dashboard to highlight our AI app integrations.
Deep Dive: Handling Dynamic Route Parameters in Next.js 15
One of the most notable differences in Next.js 15 is that parameter parameters (params and searchParams) inside dynamic pages must be treated as asynchronous promises. Let's look at an example to see how to implement dynamic routes in a real-world scenario.
Let's assume we want to create a page displaying specialized properties for individual AI models, accessible via /dashboard/[modelId]. Create a route structure at apps/client/src/app/dashboard/[modelId]/page.tsx:
// apps/client/src/app/dashboard/[modelId]/page.tsx
import Link from 'next/link';
interface PageProps {
params: Promise<{ modelId: string }>;
}
export default async function ModelDetailsPage({ params }: PageProps) {
// Await the dynamic params object per Next.js 15 standards
const resolvedParams = await params;
const { modelId } = resolvedParams;
return (
<div className="bg-white border border-slate-200 rounded-xl p-8 max-w-xl mx-auto space-y-6">
<div className="space-y-2">
<span className="text-xs font-semibold text-indigo-600 tracking-wider uppercase">Model Configuration</span>
<h2 className="text-2xl font-bold text-slate-950">Active Node: {modelId}</h2>
</div>
<p className="text-slate-600 leading-relaxed text-sm">
This dashboard page asynchronously extracts the routing context via Next.js 15 parameters.
In Part 3, we will bind this ID dynamically to our backend NestJS api endpoint to fetch live telemetry.
</p>
<div className="pt-4 border-t border-slate-100 flex justify-between items-center">
<Link href="/" className="text-sm font-medium text-slate-500 hover:text-slate-800 transition-colors">
← Back to home
</Link>
<span className="px-3 py-1 bg-indigo-50 text-indigo-700 font-semibold text-xs rounded-full border border-indigo-100">
Status: Ready
</span>
</div>
</div>
);
}
Using this setup, Nx understands that client utilizes server dynamic paths, optimizing the code splitting and packaging pipelines without manual build adjustments.
Creating and Importing Shared Libraries
One of the primary benefits of an Nx Monorepo is the ability to write code once and share it seamlessly across applications. Let's create a shared UI library containing a reusable component that our client application can import.
Step 1: Generate the Shared Library
Use the @nx/react generator to create a TypeScript-based library for shared interface structures:
nx g @nx/react:lib ui-shared --directory=libs/ui-shared
Nx automatically modifies tsconfig.base.json to map path mappings, making our shared library importable workspace-wide using aliases like @myworkspace/ui-shared.
Ensure your component is exported through your library's entry point, libs/ui-shared/src/index.ts:
// libs/ui-shared/src/index.ts
export * from './lib/ui-shared';
Step 4: Importing Your Shared Component in the Next.js Client App
Now, let's use our shared component within our client-side dashboard page:
// apps/client/src/app/dashboard/page.tsx
'use client';
import { SharedCard } from '@my-workspace/ui-shared';
export default function DashboardIndexPage() {
const handleAction = (modelName: string) => {
alert(`Connecting interface parameters to: ${modelName}`);
};
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">System Telemetry Controls</h2>
<p className="text-sm text-slate-500">Interact with model microservices in the shared repository space.</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<SharedCard
title="GPT-4o Telemetry"
description="Real-time natural language analysis node. Controls parsing speed settings and limits input sequences dynamic load rules."
status="active"
actionLabel="Access Console"
onAction={() => handleAction('GPT-4o')}
/>
<SharedCard
title="Stable Diffusion XL"
description="GPU cluster monitoring module. Optimizes cold startup times and limits image generation execution threads."
status="development"
actionLabel="Launch Sandbox"
onAction={() => handleAction('Stable Diffusion XL')}
/>
<SharedCard
title="Whisper Speech v3"
description="Legacy translation endpoint. Deprecation scheduled for mid-year as backend modules transition to direct WebSockets API engines."
status="deprecated"
/>
</div>
</div>
);
}
Running the Task Pipelines
Nx optimizes project management through a streamlined command line utility. Here are the core workspace commands for developing, testing, and building your Next.js frontend application.
1. Developing locally
Run your development server with hot-reload enabled. Nx serves your application at http://localhost:4200 by default:
nx serve client
2. Building for production
Compile and bundle your client app for production deployment. Nx handles dynamic assets and outputs optimized production-ready code to the workspace's distribution folder (dist/apps/client):
nx build client
3. Testing
Execute unit tests using your pre-configured testing frameworks (Vitest or Jest):
nx test client
4. Code Quality and Formatting
Maintain clean, formatting-compliant source files across the monorepo by running global checks:
nx lint client
nx format:write
Next.js 15 Caching and Build Optimization in Nx
Next.js 15 introduces dynamic caching updates that integrate seamlessly with Nx's local and remote build caching tools. Here are three key strategies to optimize your builds:
1. Understanding Next.js 15 Cache Updates
In Next.js 15, GET fetch requests are no longer cached by default. If your frontend components query remote endpoints, your application will dynamic-render pages on request unless explicitly opted into caching. Use configuration overrides when implementing custom caching parameters:
// Example of opting back into caching for standard requests
fetch('https://api.aihub.com/models', { cache: 'force-cache' });
2. Leverage Incremental Rebuilds via nx affected
In a standard monorepo setup, simple client-side edits can trigger full repository builds, which increases pipeline runtimes. Nx solves this issue through static code graph tracking.
When a developer modifies files inside the apps/client directory without touching our libs/ui-shared directory, running the following command ensures that only the frontend application undergoes CI testing and deployment builds:
nx affected:build
3. Integrating React Compiler
Next.js 15 integrates the React Compiler out of the box to streamline rendering optimizations. To enable compilation verification for your Nx application, add the experimental flag to next.config.js:
How does Next.js 15 manage TypeScript configuration differences in an Nx monorepo?
Next.js applications generate and manage their own local tsconfig.json files, which extend the root tsconfig.base.json path configurations. This configuration allows Next.js 15 to manage its custom build configuration options, while ensuring your app can import code from shared workspace libraries.
Can I share Tailwind styles from my client app into dynamic components inside my shared libraries?
Yes. In your tailwind configuration file (apps/client/tailwind.config.js), simply add the paths to your shared libraries under the content property. This ensures Tailwind parses the shared components and applies the correct styles during compilation:
Why does my build fail with dynamic variables on server components?
In Next.js 15, dynamic parameters inside page headers are asynchronous. When parsing elements, ensure you handle dynamic components using standard async-await syntax, as shown below:
How does Nx Cloud help speed up builds in large monorepos?
Nx Cloud enables remote caching across your development team. This means that if a colleague (or your CI pipeline) has already built or tested a specific project or component, your machine can download the cached output instead of rebuilding it from scratch, significantly reducing build times.
What's Next?
Congratulations! You have successfully built a robust Next.js 15 frontend featuring the dynamic App Router inside your custom Nx Monorepo. By adding shared library pipelines, you're ready to scale your application's user interface with highly decoupled, reusable code.
In Part 3 of our series, we will build out a NestJS backend within our monorepo and connect it to our Next.js frontend to enable dynamic, type-safe data communication. Stay tuned!
How to Use AI for Content Creation: Best Practices
Discover how to leverage AI for content creation without losing your brand's voice or hurting your SEO. This comprehensive guide covers the Human-in-the-Loop workflow, Python integration, style prompts, and E-E-A-T compliance.