RORK LABJP
DEADLINE — From August 31, every new app and app update on Google Play must target Android 16 (API level 36). Eight days to goEXTENSION — Eligible developers can request an extension through November 1 via a form in Play Console, but the request itself has to be filed before the deadlineEXISTING — Even apps you leave alone need at least API level 35 to stay discoverable to new users on recent Android devices. Standing still quietly cuts off new installsEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 as a small, one-command upgrade with no breaking changes. React stays at 19.2, same as SDK 56MEMORY — expo@57.0.9 bumps React Native to 0.86.2 and clears the Hermes V1 memory regression that inflated usage in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native directories by default, which collides easily with hand-edited config from your API 36 migrationDEADLINE — From August 31, every new app and app update on Google Play must target Android 16 (API level 36). Eight days to goEXTENSION — Eligible developers can request an extension through November 1 via a form in Play Console, but the request itself has to be filed before the deadlineEXISTING — Even apps you leave alone need at least API level 35 to stay discoverable to new users on recent Android devices. Standing still quietly cuts off new installsEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 as a small, one-command upgrade with no breaking changes. React stays at 19.2, same as SDK 56MEMORY — expo@57.0.9 bumps React Native to 0.86.2 and clears the Hermes V1 memory regression that inflated usage in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native directories by default, which collides easily with hand-edited config from your API 36 migration
Articles/Dev Tools
Dev Tools/2026-07-09Advanced

Holding Layer Boundaries in a Rork-Generated Expo App with ESLint and dependency-cruiser

Long-lived Rork-generated Expo apps quietly accumulate screens that import the API client directly. Here is how I froze 214 existing violations as a baseline, eliminated 17 circular dependencies, and made CI reject anything new for 38 extra seconds.

Rork543Expo180Architecture22ESLintCI5

Premium Article

I opened a screen component I had not touched in six months and found import { supabase } from "../../lib/supabaseClient" sitting at the top.

That screen was supposed to receive its data through a hook. But across rounds of generation and hand-editing, the shortest path had quietly won. There is nobody to blame. I am the one who once said "just this once, just here" — and that single line got read back by the next generation pass, then copied into the next screen.

When you maintain several apps as an indie developer, this kind of decay happens without a sound. No reviewer is watching. So the reviewer has to become a machine.

Let me say up front what this article does not cover. It is not an argument about the ideal layer architecture. It is about taking an app already live on the App Store, keeping its current structure, and making sure it stops getting worse.

Declare the boundary before you defend it

The first thing that tripped me up was that nobody had written down where the boundaries were. They lived in my head. They did not live in any file.

So I kept the existing directory layout untouched and simply wrote the allowed direction of dependency into a single table. No new architecture invented. Just names given to what already existed. That choice alone changed the size of the job.

LayerPathMay import
app (screens)app/**features, ui, shared
featuressrc/features/**own feature, data, ui, shared
data (fetch and persistence)src/data/**shared only
ui (presentational)src/ui/**shared only
shared (types, constants, pure functions)src/shared/**nothing (leaf)

The interesting constraint is the ban on feature-to-feature imports. Ask Rork to "show an unread badge on the favorites screen" and you get code where features/favorites reaches straight into features/notifications. It works. It works right up until the day you want to rebuild one of them and discover the other is a hostage.

If the crossing is genuinely needed, the type belongs in shared and the query belongs in data. The whole exercise is about moving decisions out of places where a decision has to be made every time.

Make the wrong import unwriteable

ESLint's no-restricted-imports earned its place first, because the red squiggle appears in the editor. I notice it the moment the code is generated — a full day before CI would have told me.

With Flat Config, each zone gets its own block.

// eslint.config.js
import tseslint from "typescript-eslint";
 
/** Import patterns forbidden per layer */
const layerRules = {
  "src/data/**": ["@/features/*", "@/ui/*", "@/app/*"],
  "src/ui/**": ["@/features/*", "@/data/*", "@/app/*"],
  "src/shared/**": ["@/features/*", "@/data/*", "@/ui/*", "@/app/*"],
  "app/**": ["@/data/*"], // screens never touch the data layer directly
};
 
export default tseslint.config(
  ...Object.entries(layerRules).map(([files, patterns]) => ({
    files: [files],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: patterns.map((group) => ({
            group: [group],
            message: `${files} may not import from this layer. Route it through shared instead.`,
          })),
        },
      ],
    },
  })),
);

Write the message carefully. Six months from now you will not remember why the rule exists, and that error string becomes the only design document that survives. In practice this mattered more than I expected: without a message, developers (me included) reach for // eslint-disable-next-line. With one, they reach for the correct import.

ESLint alone is not enough, though. A relative escape like ../../data/client from app/screens/Home.tsx can slip past a pattern, and circular dependencies are invisible to a rule that only sees one line at a time.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
The baseline technique that froze 214 legacy violations while forcing new ones to zero
Working ESLint zone config and dependency-cruiser rules, including the $1 back-reference for cross-feature bans
Measured cost: 38 seconds of CI time, 17 circular dependencies removed in three weeks
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.
Dev Tools2026-06-22
When a Poisoned Cache Crashes Your App on Every Launch — Designing a Safe-Mode Boot Your Users Can Escape On Their Own
When a persisted cache goes bad and the app crashes at the same spot on every launch, the only option left to the user is to reinstall. This article designs a safe-mode boot for Expo (React Native): the app counts its own early crashes, confirms a launch only once it becomes interactive, and resets just the dangerous state in graduated steps.
Dev Tools2026-06-15
Drawing the Line Between Rork Max's Swift Output and the Expo Build
Rork Max now generates native Swift, while the standard Rork keeps producing Expo (React Native) apps. Here is how to split responsibilities between the two engines inside a single app business, viewed from real maintenance cost.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →