Contact us
Contact us
01. Our Work
01. Our Work
02. About Us
02. About Us
03. Solutions
03. Solutions
04. Our Process
04. Our Process
05. Blog
05. Blog
06. Calculator
06. Calculator

Our offices

  • Tallinn
    Harju maakond, Kesklinna linnaosa, Narva mnt 5
    10117, Tallinn, Estonia
    • +372-623-7083
  • Email
    office@make-it.run

Follow us

  • Work
    • View Our Work
    • Case Studies
    • See all →
  • Company
    • About
    • Solutions
    • Process
    • Blog
    • Calculator
    • Contact us
  • Legal
    • Privacy Policy
    • Terms of Service
  • Connect
    • LinkedIn
    • Facebook
    • Youtube
    • X

Stay updated with make-it.run

Subscribe to get the latest tech insights, startup resources, and development tips from our team.

© make-it.run 2025

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

Aleksandr

By Aleksandr

Published on July 2025

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

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"

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

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.

Top tip

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:

// 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

Top tip

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>

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/*"
      ]
    }
  }
}

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

{
  "include": [
    "src/**/*",
    "../../libs/**/*.ts",
    "../../libs/**/*.tsx"
  ]
}

ESLint Configuration for Module Boundaries

Install the required NX ESLint dependencies:

nx add @nx/eslint-plugin @nx/devkit

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',
  },
}

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.

Top tip

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

Verify the presence of the NX React package:

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:

// 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:

// 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:

/* apps/web/src/app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Import the global stylesheet in your layout component:

// 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:

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

Create the NativeWind environment declaration:

// apps/mobile/nativewind-env.d.ts
/// <reference types="nativewind/types" />

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

// 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:

// 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:

// 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:

// 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:

// 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:

// 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
}
GitHub repo:
makeit-run/monorepo-heaven
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.

Tell us about your project

Tell us everything!

Our offices

  • Tallinn
    Harju maakond, Kesklinna linnaosa, Narva mnt 5
    10117, Tallinn, Estonia
    • +372-623-7083
  • Email
    office@make-it.run

Related Posts

Complete Guide to Setting Up NX  + Next.js + Expo Project: Modern Monorepo Architecture. Part 1
July 2025/Blog Post

Complete Guide to Setting Up NX + Next.js + Expo Project: Modern Monorepo Architecture. Part 1

Aleksandr
By Aleksandr