DEV Community

Cover image for Setting Up ESLint, Prettier, Husky & Architecture Rules in a React Native TypeScript Project
Omotola Odumosu
Omotola Odumosu

Posted on

Setting Up ESLint, Prettier, Husky & Architecture Rules in a React Native TypeScript Project

Maintaining code quality becomes increasingly difficult as a React Native project grows However, this is very important at an enterprise level. Scalability should never endanger the quality of your codebase. By combining ESLint, Prettier, Husky, lint-staged, and architecture enforcement through eslint-plugin-boundaries, you can automate formatting, catch errors early, and prevent architectural violations across your codebase, so irrespective of changes in developers working on the project, the codebase never becomes messy, and with the predefined rule, it enforces itself.
 
This guide walks through the complete setup using ESLint Flat Config (eslint.config.mjs) and a feature-based architecture.

Before We Begin

Before we dive into the manual setup, it's worth mentioning that there's an easier way.

I packaged this entire setup into a reusable React Native template that configures ESLint, Prettier, Husky, lint-staged, and feature-based architecture boundaries for you, giving you enterprise-grade code quality and architectural protection with a single command.

npx @react-native-community/cli init MyApp --template react-native-template-feature-boundary
Enter fullscreen mode Exit fullscreen mode

Documentation 👉 React Native Feature Boundary

If you'd like to get started quickly, the template is the fastest option.

However, if you're curious about how everything works under the hood, or you prefer configuring it yourself, let's walk through the complete setup step by step.


1. Install Dependencies

Run the following command from the root of your project:

npm install --save-dev eslint@^9.0.0 @eslint/js@^9.0.0 typescript-eslint@^8.0.0 eslint-plugin-react@^7.35.0 eslint-plugin-react-hooks@^5.0.0 eslint-plugin-boundaries@^6.0.2 eslint-config-prettier@^10.1.8 prettier@2.8.8 husky@^9.1.7 lint-staged@^17.0.2 @react-native/jest-preset
Enter fullscreen mode Exit fullscreen mode

If you're using Yarn or pnpm, replace the install command accordingly.


2. Configure Prettier

Create a .prettierrc.js file in the project root and add:

module.exports = {
  arrowParens: 'avoid',
  singleQuote: true,
  trailingComma: 'all',
};
Enter fullscreen mode Exit fullscreen mode

This configuration enforces the following:

  • Single quotes
  • Trailing commas
  • Cleaner arrow function syntax.

3. Configure ESLint Flat Config

Create an eslint.config.mjs file in the project root and paste the below code into the file.

This configuration provides:

  • JavaScript recommended rules
  • TypeScript recommended rules
  • React linting
  • React Hooks validation
  • Feature-based architecture enforcement
  • Prettier compatibility
  • Build/config file exclusions
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import boundaries from 'eslint-plugin-boundaries';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,

  {
    files: ['**/*.{js,jsx,ts,tsx}'],

    plugins: {
      react: reactPlugin,
      'react-hooks': reactHooksPlugin,
      boundaries,
    },

    languageOptions: {
      ecmaVersion: 2022,
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },

    settings: {
      react: {
        version: 'detect',
      },

      'import/resolver': {
        node: {
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
        },
      },

      'boundaries/elements': [
        // Must be first so index files are classified as public API,not as internal files of the feature folder.
        {
          type: 'feature-public',
          mode: 'file',
          pattern: 'src/feature/*/index.*',
          capture: ['featureName'],
        },

        {
          type: 'feature-internal',
          pattern: 'src/feature/*',
          mode: 'folder',
          capture: ['featureName'],
        },

        {
          type: 'shared',
          pattern: 'src/shared/**',
        },

        {
          type: 'navigation',
          pattern: 'src/navigation/**',
        },

        {
          type: 'redux',
          pattern: 'src/Redux/**',
        },
      ],
    },

    rules: {
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      'boundaries/no-unknown': 'error',

      'boundaries/dependencies': [
        'error',
        {
          default: 'disallow',

          rules: [
            // Shared and navigation modules can be imported by any element.
            {
              from: { type: '*' },
              allow: {
                to: [
                  { type: 'shared' },
                  { type: 'navigation' },
                  { type: 'redux' },
                ],
              },
            },

            // Navigation can import from feature public APIs.
            {
              from: { type: 'navigation' },
              allow: { to: { type: 'feature-public' } },
            },

            // Within the same feature, internal files can import each other freely.
            {
              from: { type: 'feature-internal' },
              allow: {
                to: {
                  type: 'feature-internal',
                  captured: {
                    featureName: '{{from.captured.featureName}}',
                  },
                },
              },
            },

            // Different feature internals forbidden
            {
              from: { type: 'feature-internal' },
              disallow: {
                to: {
                  type: 'feature-internal',
                  captured: {
                    featureName: '!{{from.captured.featureName}}',
                  },
                },
              },
              message:
                'Architecture violation: features may depend on other features only through their public API. Feature "{{from.captured.featureName}}" cannot import internal files from feature "{{to.captured.featureName}}". Replace the deep import with an import from src/feature/{{to.captured.featureName}}/index.ts',
            },

            // A feature can only import from another feature through its public API (index.ts).
            {
              from: { type: 'feature-internal' },
              allow: { to: { type: 'feature-public' } },
            },

            // Public API files can import their own feature's internals to re-export them.
            {
              from: { type: 'feature-public' },
              allow: {
                to: {
                  type: 'feature-internal',
                  captured: {
                    featureName: '{{from.captured.featureName}}',
                  },
                },
              },
            },

            // Public API files can import from other features' public APIs.
            {
              from: { type: 'feature-public' },
              allow: { to: { type: 'feature-public' } },
            },
          ],
        },
      ],
    },
  },

  {
    ignores: [
      'node_modules/**',
      'android/**',
      'ios/**',
      '.bundle/**',
      'vendor/**',

      // config files
      '.prettierrc.js',
      'babel.config.js',
      'jest.config.js',
      'metro.config.js',
    ],
  },

  eslintConfigPrettier,
);
Enter fullscreen mode Exit fullscreen mode

Note: if your file path are different, you can modify this config in the "boundaries/elements" section to meet your project's specific paths. More insight in Step 8 and 9


4. Update package.json

In your existing package.json, locate the "scripts" section and add the following entries to it:

Important: Your package.json already contains a "scripts" object. Do not replace it or add another "scripts" object. Simply add these three scripts alongside your existing ones.

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "prepare": "husky"
  }
}
Enter fullscreen mode Exit fullscreen mode

Example: Here's how your script should look; it might not be exactly the same but should be similar.

Before

After

After the "scripts" section, add the following top-level "lint-staged" configuration.

Important: Add "lint-staged" as a sibling of "scripts" in your package.json, not inside the "scripts" object.

{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": [
      "prettier --write",
      "eslint --fix --cache"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Example:


5. Initialize Husky

Run:

npx husky init
Enter fullscreen mode Exit fullscreen mode

This command:

  • Creates the .husky directory
  • Adds the prepare script if missing
  • Creates a default Git hook

6. Configure the Pre-Commit Hook

Create or replace .husky/pre-commit with:

npx lint-staged
Enter fullscreen mode Exit fullscreen mode

No arguments are required.

lint-staged automatically detects staged files and runs the configured tasks.


7. Ignore ESLint Cache

Add the following to .gitignore:

# ESLint cache
.eslintcache
Enter fullscreen mode Exit fullscreen mode

This prevents ESLint's cache file from being committed.


8. Enforcing Feature-Based Architecture

The setup uses eslint-plugin-boundaries to prevent modules from bypassing your project's public APIs.

Folder Structure

Element Type     Pattern               Purpose                            
feature-public   src/feature/*/index. Public API of a feature            
feature-internal src/feature/*         Internal feature implementation    
shared           src/shared/**         Shared components, hooks, utilities
navigation       src/navigation/**     Navigation layer                   
redux            src/Redux/**          Global state management            

Import Rules:

Allowed

✅ Any module/feature can import from:

  • shared
  • navigation
  • redux

✅ Navigation can import feature public APIs.
 
✅ Files inside the same feature can import each other.
 
✅ Feature public APIs (index.ts) can import their own internals.
 
✅ Feature public APIs can import other feature public APIs.

Forbidden

❌ Feature internals cannot import another feature's internals.

Example:

// Not allowed
import { Something } from '@/feature/orders/components/Something';
Enter fullscreen mode Exit fullscreen mode

Instead:

// Allowed
import { Something } from '@/feature/orders';
Enter fullscreen mode Exit fullscreen mode

This ensures all cross-feature communication goes through the feature's public API.


9. Adapting the Configuration

If your project uses a different folder structure, update the paths in boundaries/elements.

For example, if features live in src/modules:

{
  type: 'feature-public',
  mode: 'file',
  pattern: 'src/modules/*/index.*',
  capture: ['featureName'],
},
{
  type: 'feature-internal',
  pattern: 'src/modules/*',
  mode: 'folder',
  capture: ['featureName'],
},
Enter fullscreen mode Exit fullscreen mode

You can also add additional architectural layers such as:

  • src/api
  • src/components
  • src/services
  • src/design-system

Simply create new element types and corresponding dependency rules.


10. Verify Everything Works (optional)

Check ESLint

npx eslint --version
Enter fullscreen mode Exit fullscreen mode

Run Linting

npm run lint
Enter fullscreen mode Exit fullscreen mode

Test lint-staged

npx lint-staged
Enter fullscreen mode Exit fullscreen mode

Test a Commit

git add .
Enter fullscreen mode Exit fullscreen mode
git commit -m "test: verify husky lint-staged"
Enter fullscreen mode Exit fullscreen mode

During the commit process:

  1. Prettier formats staged files.
  2. ESLint fixes issues where possible.
  3. Architecture rules are validated.
  4. The commit proceeds only if all checks pass.

Files Created or Modified

File                Purpose                           
.prettierrc.js    Prettier formatting rules         
eslint.config.mjs ESLint Flat Config                
package.json      Scripts, lint-staged, dependencies
.husky/pre-commit Git pre-commit hook               
.gitignore        Ignore ESLint cache               

Using This Setup Outside React Native

For a Node.js, Next.js, or other TypeScript project:

  • Remove:

   * eslint-plugin-react
   * eslint-plugin-react-hooks

  • Remove React-specific ESLint settings and rules.

  • Adjust ignored folders to match your build outputs.

The architecture enforcement, Husky integration, lint-staged workflow, and Prettier setup remain exactly the same.


Conclusion

This setup gives you:

  • Automated formatting with Prettier
  • Consistent linting with ESLint
  • Git hook protection through Husky
  • Faster checks using lint-staged caching
  • Strict feature boundaries with eslint-plugin-boundaries

The result is a cleaner, more maintainable codebase that scales without allowing architectural shortcuts.


Did this post help simplify things for you?

If yes, drop a ❤️ or 🦄 reaction and follow me here on dev.to. I share more practical, plain-English breakdowns like this.

You can also connect with me on social media. I’d love to learn, share, and grow together with you!

LinkedIn: LinkedIn
Twitter: Twitter
Instagram: Instagram

Top comments (0)