Home / Articles

Why Codegen Makes Spec-First Mandatory for React Native TurboModules

See how React Native Codegen turns the TurboModule TypeScript spec into a build-time contract, what breaks when you skip it, and where the rule stops applying.

Publisher: REACTAPP.TOP8 min read1819 words

This article is published in English.

Sources: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It

Many teams that maintain React Native libraries keep a rule in their contributing guide: change the TurboModule spec before touching any native code. It reads like a style preference, the kind of convention you hope contributors respect under deadline pressure. On the New Architecture it is nothing of the sort. Codegen derives the native bridge from the spec, so the build itself decides the order of work. This article explains how that contract is enforced, what actually happens on iOS and Android when someone tries the shortcut, and which kinds of modules fall outside it.

A convention that turns out to be a constraint

Consider a maintainer who decides to save time: add the native method first, then go back and update the spec. On a Codegen-backed module, that plan fails before it gets anywhere useful. No linter or reviewer has to object; the compiler and the generated code do it. The realisation that follows is the important part: the rule was never a team habit that the toolchain happens to tolerate. It is a property of the toolchain that the team happened to write down.

The difference is more than wording. A preference can be traded off against a deadline. A build-time contract cannot, and knowing which one you are dealing with changes how you plan changes to a library.

The spec file at the centre of a TurboModule

Every New Architecture TurboModule has a single TypeScript file, conventionally src/Native<Name>.ts, that declares each method crossing the boundary between JavaScript and native code. A wrapper around a social login SDK might call it src/NativeNaverLogin.ts; a wrapper for in-app store reviews might use src/NativeInAppReview.ts. A codegenConfig entry in the package's package.json tells Codegen where to find that file and which kind of artefact to generate from it.

Stated as a rule, the ordering is simple: any method that JavaScript should be able to call is declared in the spec first, and only then implemented natively. Spec, then native code, never the other way round. What makes that ordering non-negotiable is the machinery that consumes the spec.

How Codegen turns the spec into native obligations

Codegen is a transformation that runs at build time. It parses the TypeScript spec and produces the bridge surface for each platform:

  • Android: an abstract class, for example NativeNaverLoginSpec. Your Kotlin or Java module extends this generated class rather than extending ReactContextBaseJavaModule directly, and every method in the spec becomes an abstract method you must implement.
  • iOS: Objective-C++ JSI glue, including a NativeNaverLoginSpecJSI translation unit and a protocol that your .mm implementation must conform to.

The key detail is the direction of dependency. Native code does not describe the bridge; it inherits a bridge that was generated from the spec. JavaScript, in turn, reaches native code only through that generated surface. That single fact determines what happens in each of the two ways the sides can drift apart.

A native method the spec does not declare

If you add a method on the native side without adding it to the spec, Codegen has generated nothing for it. There is no abstract method to override on Android and no protocol entry on iOS.

On Android, the most common way to write such a method is with override, which fails to compile because the generated parent class has no matching member. Even if you drop the modifier and the file compiles, the method is not part of the generated surface, so JavaScript has no path to it.

On iOS the failure is quieter. The method exists in your .mm file and compiles cleanly, but it is not in the protocol the runtime uses to expose methods, so JavaScript can never reach it. A silent dead method is arguably worse than a compile error: nothing tells you it is broken until someone calls it from JS and finds it missing.

A spec method with no native implementation

The opposite drift fails too. If the spec declares a method but the native side does not implement it, Codegen still emits the bridge for it, and the JavaScript proxy happily exposes the method.

On Android, a class that extends the generated abstract spec without implementing one of its abstract methods is rejected by the Kotlin or Java compiler, which is the best case because it fails early. On iOS, a missing required protocol method typically only produces a compiler warning; the real failure arrives at runtime as an unrecognised-selector crash when JavaScript calls the method.

Either way, the two halves are bound together when code is generated. You cannot give JavaScript a new capability by editing only one side. What stops you is not a review comment but the type system and the generated glue refusing to line up.

Why the rule applies to every Codegen library

If spec-first were merely a house style, it would only apply in the repositories whose maintainers chose it. But the enforcement comes from Codegen, and Codegen belongs to React Native. Every library that enables Codegen with a codegenConfig block on the New Architecture is subject to the same mechanics, whether or not its authors ever thought in terms of "spec first".

The official React Native documentation reflects this in its wording. Describing the output folder that Codegen writes into, it presents the generated abstract class as something a Turbo Native Module is required to implement. Required is the operative word: that is an obligation checked when the code compiles, not a recommendation. Community walkthroughs describe the same behaviour from the outside: once codegenConfig is wired up, a generated Native...Spec file appears, named after whatever codegenConfig specifies in package.json. Different authors, different repositories, identical contract.

There is also a useful confirmation from the opposite direction. Leave the codegenConfig field out of package.json altogether, and running a CocoaPods install on iOS reports the gap:

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

Codegen then skips that package entirely. The pipeline is built to stop rather than guess at a bridge. The spec is not an optional document that can fall out of date; it is the input the whole generation step is organised around.

Put together, spec-first is a structural consequence of how the pipeline reads its input. Libraries follow it because they follow Codegen's shape. A library that appears to ignore it is either still on the legacy architecture or has not configured codegenConfig at all.

Where the contract stops applying

Describing this as a toolchain contract is accurate, but it is easy to stretch the claim too far. The boundaries are worth stating plainly:

  • Legacy bridge modules. Modules built on the pre-JSI bridge with RCTBridgeModule on iOS or ReactContextBaseJavaModule on Android, with no spec, predate Codegen. They have no generated source of truth, so nothing mechanical enforces the ordering. Discipline is all you have there.
  • Packages without codegenConfig. Without that block, no artefacts are generated, and the chain from spec to native obligation never starts.
  • Fabric view components. Components declared with type: "components" also go through Codegen, so it is reasonable to expect a similar discipline. However, the reasoning above is grounded in Turbo Native Modules (type: "modules"), and the generated artefacts for components differ, so treat the components case as likely rather than established and verify it against the current documentation for your React Native version.

The claim that holds up firmly is narrow: for New Architecture TurboModules generated by Codegen from a JavaScript or TypeScript spec, which is the officially documented way to author them, spec-first is enforced by the build.

It is also worth noting that Codegen-based TurboModules are not the only way to write native modules today. Alternatives such as Nitro Modules use their own code generation and binding model; the article on how Nitro's static bindings compare with TurboModules covers that trade-off.

The workflow the build actually enforces

Once the mechanism is clear, the order of work stops being a matter of judgement. Adding a method to a TurboModule follows one direction:

  1. Edit the spec in src/Native<Name>.ts to declare the new method and its types.
  2. Run Codegen: pod install regenerates the iOS glue, and a Gradle sync or build regenerates the Android artefacts.
  3. Implement the freshly generated signatures on each platform. The Android and iOS steps are independent and can happen in parallel, but both depend on step 2.
  4. Re-export the method from the library's public JavaScript API.

A few practical habits follow from this:

  • Review spec changes as API changes. The spec is effectively the public native contract of the library, so changes to it deserve the same scrutiny as a change to exported JavaScript types.
  • Regenerate before debugging. When a native method seems not to exist, stale generated code from a skipped pod install or Gradle sync is a common cause.
  • Run both platform builds in CI. Because iOS can fail silently where Android fails loudly, a green Android build alone does not prove the bridge is complete.

Wrapping up

Knowing who "owns" the spec-first rule is not pedantry. Once you see that it is mechanical, you stop treating it as advice to weigh against a deadline. There is no "just this once" version of adding the native method first and fixing the spec later, because the shortcut does not produce working code.

  • Codegen generates the native bridge from the TypeScript spec, so native code implements the spec rather than defining the bridge.
  • Drift in either direction fails: sometimes at compile time, sometimes silently on iOS, sometimes as a runtime crash.
  • The contract covers Codegen-backed TurboModules; legacy bridge modules and packages without codegenConfig sit outside it, and Fabric components need their own verification.
  • Keep "spec first" in your contributing guide, but phrase it as a description of how the build works rather than a request for good behaviour. The build will enforce it regardless of what the document says.