DEV Community

Cover image for Complete Guide to Setting Up NX + Next.js + Expo Project: Modern Monorepo Architecture. Part 2 (Tailwind configuration)
Devops Makeit-run
Devops Makeit-run

Posted on • Edited on • Originally published at make-it.run

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 recommendations

Recommended 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
Enter fullscreen mode Exit fullscreen mode

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:

// apps/web/src/app/page.tsx
export { default } from "@frontend/feature-home/web/pages/page"
Enter fullscreen mode Exit fullscreen mode

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:

nx g lib feature-name
Enter fullscreen mode Exit fullscreen mode

The recommended libs structure follows this hierarchical organization:

libs/
├── backend/
│   ├── feature-home/
│   ├── feature-dashboard/
│   └── feature-user/
├── frontend/
│   ├── feature-home/
│   ├── feature-dashboard/
│   └── feature-user/
└── shared/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

// libs/frontend/feature-auth/shared/src/lib/constants.ts
export const Greeting = "How can I help you today?"
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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


Maintain the one-page-one-section rule to preserve clear architectural boundaries. For complex scenarios requiring multiple sections, compose them at the page level rather than nesting sections within sections.

Advanced Section Composition

For edge cases requiring multiple sections (such as location search with results display), implement composition patterns:

// page.tsx
<SearchDataSection data={initialData}>
  <LocationSearchSection/>
</SearchDataSection>
Enter fullscreen mode Exit fullscreen mode

TypeScript Configuration for Clean Imports

Update your path aliases in tsconfig.base.json to enable clean import statements:

{
  "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/*"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Configure TypeScript compilation resolution in each app's tsconfig.json:

{
  "include": [
    "src/**/*",
    "../../libs/**/*.ts",
    "../../libs/**/*.tsx"
  ]
}
Enter fullscreen mode Exit fullscreen mode

ESLint Configuration for Module Boundaries

Install the required NX ESLint dependencies:

nx add @nx/eslint-plugin @nx/devkit
Enter fullscreen mode Exit fullscreen mode

Configure ESLint to allow flexible imports between shared and platform-specific folders:

// eslint.config.js
{
  files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
  rules: {
    '@nx/enforce-module-boundaries': 'off',
  },
}
Enter fullscreen mode Exit fullscreen mode

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 uses tailwindcss: "^3.4.17" for optimal compatibility with NX workspace configurations.

Essential Dependencies Installation

Install the required Tailwind CSS dependencies:

npm add -D tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/postcss
Enter fullscreen mode Exit fullscreen mode

Verify the presence of the NX React package:

npm add @nx/react
Enter fullscreen mode Exit fullscreen mode

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:

// 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: []
}
Enter fullscreen mode Exit fullscreen mode

Create the PostCSS configuration:

// apps/web/postcss.config.js
const { join } = require("path")

module.exports = {
  plugins: {
    tailwindcss: {
      config: join(__dirname, "tailwind.config.js")
    },
    autoprefixer: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

Import Tailwind directives in your global stylesheet:

/* apps/web/src/app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Import the global stylesheet in your layout component:

// apps/web/src/app/layout.tsx
import './global.css';
Enter fullscreen mode Exit fullscreen mode

This completes the Tailwind CSS setup for your web application.

Mobile Application Tailwind Configuration

Install the mobile-specific dependencies:

npm add nativewind@^4.1.23 babel-plugin-module-resolver@^5.0.2 react-native-reanimated@~3.17.4
Enter fullscreen mode Exit fullscreen mode

Create the NativeWind environment declaration:

// apps/mobile/nativewind-env.d.ts
/// <reference types="nativewind/types" />
Enter fullscreen mode Exit fullscreen mode

Include the declaration in your mobile app's TypeScript configuration:

// apps/mobile/tsconfig.json
{
  "include": [
    "src/**/*",
    "../../libs/**/*.ts",
    "../../libs/**/*.tsx",
    "nativewind-env.d.ts"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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"]
        }
      ]
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Update the Metro configuration for comprehensive asset handling:

// 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" }))
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind for mobile development:

// 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: []
}
Enter fullscreen mode Exit fullscreen mode

Create the PostCSS configuration for mobile:

// apps/mobile/postcss.config.js
const { join } = require("path")

module.exports = {
  plugins: {
    tailwindcss: {
      config: join(__dirname, "tailwind.config.js")
    },
    autoprefixer: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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
}
Enter fullscreen mode Exit fullscreen mode

name="monorepo-heaven"
owner="makeit-run"
language="TypeScript" />

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.

Top comments (0)