Complete Guide to Setting Up NX + Next.js + Expo Project: Modern Monorepo Architecture. Part 2 (Tailwind configuration)


In our previous article, we established the foundational structure for our monorepo. Complete Guide to Setting Up NX + Next.js + Expo Project: Modern Monorepo Architecture. Part 1
Now we'll explore advanced code organization patterns and Tailwind CSS configuration strategies that will elevate your development workflow to production standards.
What You'll Learn
Advanced libs folder architecture and conventions Feature-based development patterns with shared components Cross-platform Tailwind CSS implementation TypeScript path mapping and ESLint configuration Production-ready code structure recommendationsRecommended Code Structure Conventions
As outlined in our previous article, the foundational folder structure follows this pattern:
monorepo-heaven/
├── apps/
│ ├── web/ # Next.js web application
│ ├── api/ # NestJS backend API
│ └── mobile/ # Expo mobile application
├── libs/ # Shared libraries (empty for now)
├── tools/ # Custom scripts and configurations
├── nx.json # NX workspace configuration
├── package.json # Root package management
└── tsconfig.base.json # Base TypeScript configuration
Let's examine the libs folder structure in detail, which serves as the core logic container for your entire application.
The Import-Only Pattern for Apps
We strongly recommend importing ready-to-use components and pages into your apps folder rather than implementing logic directly within applications. This approach promotes code reusability and maintainability:
typescript
// apps/web/src/app/page.tsx
export { default } from "@frontend/feature-home/web/pages/page"
This pattern ensures your application layers remain thin while business logic is properly organized in feature-specific libraries.
Comprehensive Libs Folder Architecture
Each feature should be generated as a separate library using the NX generator command for consistency and proper configuration:
bash
nx g lib feature-name
The recommended libs structure follows this hierarchical organization:
libs/
├── backend/
│ ├── feature-home/
│ ├── feature-dashboard/
│ └── feature-user/
├── frontend/
│ ├── feature-home/
│ ├── feature-dashboard/
│ └── feature-user/
└── shared/
The Critical Importance of the Shared Folder
The shared folder is essential for preventing circular dependencies within your NX application. Consider this scenario: you have a backend feature-auth folder and a frontend feature-auth folder. If you import functions from backend to frontend, and subsequently import from frontend to backend, NX will generate a circular dependency error.
The shared folder serves as a neutral zone for storing variables, helpers, and utilities that require bidirectional imports between frontend and backend modules. This architectural decision becomes crucial as your application scales.
Always place shared constants, utilities, and type definitions in the shared folder to avoid circular dependency issues. This pattern becomes increasingly important as your monorepo grows in complexity.Backend Folder Organization
Our application utilizes NestJS for backend development. Each backend feature contains resolvers, services, modules, and supporting utilities. This modular approach enables seamless inclusion or exclusion of features within your app.module, facilitating rapid feature deployment.
Here's an example structure for the feature-auth module:
libs/backend/feature-auth/
└── src/
└── lib/
├── casl/
├── helpers/
├── notifiables/
├── strategies/
├── auth.controller.ts
├── auth.module.ts
├── auth.resolver.ts
└── auth.service.ts
This organization pattern ensures instant feature delivery through simple module imports into your main application.
Frontend Folder: Cross-Platform Architecture
The frontend structure represents the most sophisticated aspect of our architecture, designed specifically for cross-platform application development:
libs/frontend/
└── feature-auth/
├── mobile/ # iOS/Android specific technologies
├── shared/ # Cross-platform implementations
└── web/ # Next.js specific technologies
Platform-Specific Implementation Guidelines
- Mobile folder: Contains platform-specific technologies that cannot be utilized in web environments (Expo APIs, expo-storage, native device features)
- Web folder: Houses Next.js-specific technologies (useRouter, cookies, server-side rendering utilities)
- Shared folder: Contains cross-platform code that functions identically across both web and mobile platforms
For example, shared constants like day names should be placed in the shared folder to prevent code duplication:
typescript
// libs/frontend/feature-auth/shared/src/lib/constants.ts
export const Greeting = "How can I help you today?"
This approach eliminates redundant code creation across mobile and web platforms.
Recommended Feature Structure: Pages, Sections, and Components
Each mobile and web folder should implement the following architectural pattern:
libs/frontend/feature-auth/
└── mobile/web/
└── src/
└── lib/
├── components/
├── pages/
└── sections/
This structure provides optimal development scalability and maintainability through clear separation of concerns:
Architectural Layer Responsibilities
Pages: Exclusively handle server-side data fetching and return section components Sections: Accept server-side data as props and contain all business logic, state management, data manipulation, and client-side queries Components: Contain pure UI implementations and accept props exclusively from sections
This architecture enables efficient debugging and development:
- Data-related issues: Check sections
- Missing data: Verify page fetch requests
- UI styling problems: Examine components
Advanced Section Composition
For edge cases requiring multiple sections (such as location search with results display), implement composition patterns:
typescript
// page.tsx
<SearchDataSection data={initialData}>
<LocationSearchSection/>
</SearchDataSection>
TypeScript Configuration for Clean Imports
Update your path aliases in tsconfig.base.json
to enable clean import statements:
json
{
"compilerOptions": {
"module": "esnext",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@frontend/feature-home/mobile/*": [
"libs/frontend/feature-home/mobile/src/lib/*"
],
"@frontend/feature-home/shared/*": [
"libs/frontend/feature-home/shared/src/lib/*"
],
"@frontend/feature-home/web/*": [
"libs/frontend/feature-home/web/src/lib/*"
]
}
}
}
Configure TypeScript compilation resolution in each app's tsconfig.json
:
json
{
"include": [
"src/**/*",
"../../libs/**/*.ts",
"../../libs/**/*.tsx"
]
}
ESLint Configuration for Module Boundaries
Install the required NX ESLint dependencies:
bash
nx add @nx/eslint-plugin @nx/devkit
Configure ESLint to allow flexible imports between shared and platform-specific folders:
javascript
// eslint.config.js
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': 'off',
},
}
This configuration prevents TypeScript errors when importing between shared and mobile/web folders while maintaining architectural integrity.
Comprehensive Tailwind CSS Installation for NX Monorepo
Building upon our previous Tailwind CSS setup, we'll extend configuration to work seamlessly across all apps and libraries within the NX workspace.
This implementation usestailwindcss: "^3.4.17"
for optimal compatibility with NX workspace configurations.
Essential Dependencies Installation
Install the required Tailwind CSS dependencies:
bash
npm add -D tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/postcss
Verify the presence of the NX React package:
bash
npm add @nx/react
We'll create separate configurations for web and mobile applications, as color schemes may align but spacing requirements will inevitably differ between platforms.
Web Application Tailwind Configuration
Configure Tailwind CSS for your Next.js web application:
javascript
// apps/web/tailwind.config.js
const path = require("path")
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind")
module.exports = {
content: [
path.join(__dirname, "src/**/*.{js,ts,jsx,tsx}"),
...createGlobPatternsForDependencies(__dirname)
],
theme: {
extend: {
colors: {
primary: "#0289df"
}
}
},
plugins: []
}
Create the PostCSS configuration:
javascript
// apps/web/postcss.config.js
const { join } = require("path")
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, "tailwind.config.js")
},
autoprefixer: {}
}
}
Import Tailwind directives in your global stylesheet:
css
/* apps/web/src/app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Import the global stylesheet in your layout component:
typescript
// apps/web/src/app/layout.tsx
import './global.css';
This completes the Tailwind CSS setup for your web application.
Mobile Application Tailwind Configuration
Install the mobile-specific dependencies:
bash
npm add nativewind@^4.1.23 babel-plugin-module-resolver@^5.0.2 react-native-reanimated@~3.17.4
Create the NativeWind environment declaration:
typescript
// apps/mobile/nativewind-env.d.ts
/// <reference types="nativewind/types" />
Include the declaration in your mobile app's TypeScript configuration:
json
// apps/mobile/tsconfig.json
{
"include": [
"src/**/*",
"../../libs/**/*.ts",
"../../libs/**/*.tsx",
"nativewind-env.d.ts"
]
}
Important: Since UI logic resides in libs folders, you must add this declaration file to every relevant library and include it in each library's tsconfig.json to prevent TypeScript compilation errors.
Configure Babel for NativeWind integration:
javascript
// apps/mobile/.babelrc.js
module.exports = function (api) {
api.cache(true)
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel"
],
plugins: [
[
"module-resolver",
{
extensions: [".js", ".jsx", ".ts", ".tsx"]
}
]
]
}
}
Update the Metro configuration for comprehensive asset handling:
javascript
// apps/mobile/metro.config.js
const { withNxMetro } = require("@nx/expo")
const { getDefaultConfig } = require("@expo/metro-config")
const { mergeConfig } = require("metro-config")
const { withNativeWind } = require("nativewind/metro")
const path = require("path")
const defaultConfig = getDefaultConfig(__dirname)
const { assetExts, sourceExts } = defaultConfig.resolver
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const customConfig = {
transformer: {
babelTransformerPath: require.resolve("react-native-svg-transformer")
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "cjs", "mjs", "svg", "ttf"]
}
}
module.exports = withNxMetro(mergeConfig(defaultConfig, customConfig), {
// Change this to true to see debugging info.
// Useful if you have issues resolving modules
debug: false,
// all the file extensions used for imports other than 'ts', 'tsx', 'js', 'jsx', 'json'
extensions: [],
// Specify folders to watch, in addition to Nx defaults (workspace libraries and node_modules)
watchFolders: []
}).then((config) => withNativeWind(config, { input: "./global.css" }))
Configure Tailwind for mobile development:
javascript
// apps/mobile/tailwind.config.js
import { join } from "path"
import { createGlobPatternsForDependencies } from "@nx/react/tailwind"
import { hairlineWidth } from "nativewind/theme"
import { lightTheme } from "../../libs/frontend/shared/feature-themeing/src/lib/themes/light/light"
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
relative: true,
files: [
join(
__dirname,
"{src,pages,components,layouts,app}/**/*!(*.stories|*.spec).{ts,tsx,html}"
),
...createGlobPatternsForDependencies(__dirname)
]
},
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
primary: "#0d4800"
}
}
},
plugins: []
}
Create the PostCSS configuration for mobile:
javascript
// apps/mobile/postcss.config.js
const { join } = require("path")
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, "tailwind.config.js")
},
autoprefixer: {}
}
}
Following this configuration, Tailwind CSS will function across both projects with platform-specific optimizations.
Additional Configuration: Image Type Declarations
To utilize various image formats in your React Native application without TypeScript errors, create this declaration file in every mobile library:
typescript
// libs/frontend/feature-home/mobile/image.d.ts
declare module "*.png" {
const value: any
export default value
}
declare module "*.jpg" {
const value: any
export default value
}
declare module "*.jpeg" {
const value: any
export default value
}
declare module "*.gif" {
const value: any
export default value
}
declare module "*.svg" {
const value: any
export default value
}
Conclusion
This comprehensive architecture establishes a robust foundation for scalable, cross-platform development within the NX ecosystem. The combination of feature-based library organization, clear separation of concerns through the pages-sections-components pattern, and unified Tailwind CSS configuration across platforms provides the infrastructure necessary for enterprise-level application development.
The architectural decisions outlined in this guide—particularly the shared folder strategy and platform-specific implementations—will prove invaluable as your team scales and your application requirements evolve. By adhering to these conventions from the outset, you ensure maintainable, testable, and performant code across your entire monorepo.
This post was originally published on make-it.run.
Subscribe to my newsletter
Read articles from Devops Make It Run directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
