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
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
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',
};
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,
);
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"
}
}
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"
]
}
}
Example:
5. Initialize Husky
Run:
npx husky init
This command:
- Creates the
.huskydirectory - Adds the
preparescript if missing - Creates a default Git hook
6. Configure the Pre-Commit Hook
Create or replace .husky/pre-commit with:
npx lint-staged
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
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';
Instead:
// Allowed
import { Something } from '@/feature/orders';
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'],
},
You can also add additional architectural layers such as:
src/apisrc/componentssrc/servicessrc/design-system
Simply create new element types and corresponding dependency rules.
10. Verify Everything Works (optional)
Check ESLint
npx eslint --version
Run Linting
npm run lint
Test lint-staged
npx lint-staged
Test a Commit
git add .
git commit -m "test: verify husky lint-staged"
During the commit process:
- Prettier formats staged files.
- ESLint fixes issues where possible.
- Architecture rules are validated.
- 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!



Top comments (0)