Spec First Isn't a House Rule — the New Architecture Toolchain Enforces It

For a while I thought "always edit the TurboModule spec before you touch native code" was just a discipline I'd imposed on my own React Native libraries. A convention. The kind of thing you write in CLAUDE.md and hope your future self respects. I even phrased it that way to myself: my house style.

Then I tried to cut the corner — add a native method first, wire up the spec later — and the build stopped me before I'd finished typing. Not a linter. Not a reviewer. The compiler. That's when it clicked that this was never my rule. It's the toolchain's, and I'd just been taking credit for it.

Here's why "spec first" is a build-time contract, not a preference — and why that distinction actually matters when you're maintaining a New Architecture library.

What the Rule Even Is

In a New Architecture TurboModule, there's one TypeScript file — src/Native<Name>.ts — that declares every method crossing the JS↔native boundary. In my Naver Login wrapper that's src/NativeNaverLogin.ts; in my In-App Review wrapper it's src/NativeInAppReview.ts. A codegenConfig block in package.json points at it:

{
  "codegenConfig": {
    "name": "NaverLoginSpec",
    "type": "modules",
    "jsSrcsDir": "src",
    "android": { "javaPackageName": "com.naverlogin" }
  }
}

The rule I kept stating imperatively was: any new method exposed to JS must be declared in the spec first, before you write the native code that implements it. Spec first, native second, never the reverse.

For years I assumed I was being fussy. Turns out I was just describing the shape of a machine.

Why It's Not Me — It's Codegen

Codegen is a build-time transformation. It reads that TypeScript spec and emits the actual bridge surface for both platforms:

  • Android gets an abstract class — NativeNaverLoginSpec — that your Kotlin module has to extend instead of extending ReactContextBaseJavaModule directly.
  • iOS gets Objective-C++ JSI glue — a NativeNaverLoginSpecJSI translation unit and a protocol your .mm file has to conform to.

Read that again with an adversarial eye, because I didn't at first. The native side doesn't define the bridge. It inherits one that Codegen generated from the spec. And that changes everything about the ordering:

If you add a native method that the spec doesn't declare, Codegen produced no glue for it. There's no abstract method to override, no protocol slot to fill. On Android you get a compile error. On iOS you get a method that exists in your .mm but is invisible to the runtime — dead code that JS can never reach, because the protocol it would've been registered against doesn't include it. The method compiles and does absolutely nothing, which is worse than an error because it fails silently.

If you declare a method in the spec whose native implementation is missing, Codegen emits the bridge stub anyway. Now the JS proxy can see the method and dispatch to it — straight into a missing-selector crash on iOS or an abstract-method exception on Android.

Either way, the two sides are yoked together at generation time. You cannot add a JS-visible method to one side without the other. That's not a code reviewer catching you. That's the type system refusing to link.

The Part That Surprised Me: It's Everyone's Rule, Not Mine

Here's what actually made me rewrite my mental model. If the rule were a house style, it'd stop at my repos. But the enforcement mechanism is Codegen, and Codegen isn't mine — it's React Native's. So the rule has to apply to every library that opts into the New Architecture through a codegenConfig block, whether the author ever thought about "spec first" or not.

The official React Native docs back this up in the wording, if you read closely. On the folder Codegen writes into, they describe the emitted file as "the abstract class that a Turbo Native Module has to implement." Has to implement is a compile-time obligation, not a suggestion. Community walkthroughs report the same mechanic from the outside — after wiring codegenConfig, you find a generated Native...Spec file "constructed based on the name specified in codegenConfig in your package.json." Same tool, same contract, someone else's repo.

And there's a lovely negative-space confirmation: if you leave codegenConfig out of package.json entirely, a CocoaPods install prints

[Codegen] The "codegenConfig" field is not defined in package.json

and skips codegen for the package. The pipeline is designed to refuse to proceed rather than guess. The spec isn't an optional artifact you can forget — it's the input the whole machine is built around.

So the rule I'd written down as personal discipline was actually a structural consequence of how the pipeline consumes its input. My repos follow it because they follow the shape of Codegen. Any repo that skips it either isn't on the New Architecture or hasn't wired codegenConfig at all.

Where the Rule Stops

I want to be honest about the edges, because "it's a toolchain contract" is easy to over-claim:

  • Legacy bridge modules (pre-JSI RCTBridgeModule / ReactContextBaseJavaModule, no spec) predate Codegen and have no equivalent source of truth. The grip that makes this rule mechanical simply isn't there.
  • Packages without a codegenConfig block produce no generated artifacts, so the whole input-to-output chain never engages.
  • Fabric view components (type: "components") almost certainly inherit the same discipline — they use Codegen too — but I'll flag that as only partially confirmed from the sources I traced, which centered on Turbo modules (type: "modules"). I haven't personally pinned a primary-source quote for the components path, so I'm not going to pretend I did.

The generalization I'm confident in is narrow and precise: New Architecture TurboModules that use Codegen from a JS spec. That's the officially documented authoring path, and within it, spec-first is enforced by the build.

The Takeaway

The reason this matters isn't pedantry about who owns the rule. It's that once you understand why it's mechanical, you stop treating it as advice you can weigh against a deadline. You can't "just this once" add the native method first and clean up the spec later, because the toolchain won't let the shortcut compile into anything that works. The corner you're trying to cut has already been welded shut.

So the workflow is genuinely one-directional: edit src/Native<Name>.ts, run Codegen (via pod install on iOS, a Gradle sync on Android), then implement the freshly emitted signatures on each platform, then re-export from your public JS surface. The Android and iOS implementation steps can run in parallel — they're per-platform — but they both come after the spec, always.

If you maintain a New Architecture library, stop writing "spec first" in your contributing guide as if it's your team's clever convention. It's not yours. It's the shape of the machine you're standing inside. Write it down anyway — but write it as a description of the build, not a request for good behavior. The build is going to enforce it whether your CONTRIBUTING.md mentions it or not.