# Building TurboModules in React Native: Writing Native Code That Actually Works

> Step-by-step guide to building React Native TurboModules with JSI — TypeScript spec, Swift and Kotlin implementation, and CodeGen setup.

- **Published:** 2025-05-08
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** react-native, turbomodules, native-modules, jsi, advanced
- **Canonical URL:** https://clixo.sh/blog/building-turbomodules-react-native-native-code

You need to access a native SDK, a hardware peripheral, or a platform API that no existing React Native package covers. Writing your own native module is the right answer — but the correct way to write one has changed significantly with the New Architecture.

The old `RCT_EXPORT_MODULE` and `@ReactMethod` pattern is deprecated. TurboModules are the replacement. This guide walks through writing a TurboModule from the TypeScript spec through to working Swift and Kotlin implementations.

## What TurboModules Are and Why They Replace the Old Bridge

The legacy native module system serialized every JavaScript call to JSON, passed it across an asynchronous bridge to the native side, and returned the result the same way. Synchronous calls were not possible, and the serialization overhead was measurable.

TurboModules use JSI (JavaScript Interface) — a C++ layer that lets JavaScript hold direct references to native objects. This means:

- Synchronous calls to native code are possible where appropriate
- No JSON serialization overhead for type-safe values
- Modules are lazy-loaded and only initialized when JavaScript first uses them
- Types flow from a single TypeScript spec through generated C++ bindings to native implementations

```mermaid
flowchart TD
  A["TypeScript Spec"] --> B[CodeGen]
  B --> C["C++ Bindings"]
  C --> D["Swift Implementation"]
  C --> E["Kotlin Implementation"]
  D --> F["JavaScript via JSI"]
  E --> F
```

## Step 1: Write the TypeScript Spec

The TypeScript spec is the source of truth for your module's interface. CodeGen reads it and generates the C++ glue code that binds your JavaScript API to the native implementations.

Create a file named `NativeMyModule.ts` (the `Native` prefix is required):

```ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getDeviceInfo(): Promise<{ model: string; osVersion: string }>;
  computeHash(input: string): string;
}

export default TurboModuleRegistry.strictGet<Spec>('MyModule');
```

Rules for the spec file:
- Filename must match `Native[ModuleName].ts`
- The interface must extend `TurboModule`
- All types must be expressible in the CodeGen type system — no union types with complex shapes, no recursive types
- Synchronous methods return values directly; async methods return `Promise`

## Step 2: Configure CodeGen

In `package.json`, add the CodeGen configuration:

```json
{
  "codegenConfig": {
    "name": "MyModuleSpecs",
    "type": "modules",
    "jsSrcsDir": "./src"
  }
}
```

Point `jsSrcsDir` to the directory containing your `NativeMyModule.ts` file.

Run `cd android && ./gradlew generateCodegenArtifactsFromSchema` to generate the Android bindings, and `cd ios && RCT_NEW_ARCH_ENABLED=1 pod install` to generate the iOS bindings.

## Step 3: iOS Implementation in Swift

Create `MyModule.swift` and `MyModuleImpl.m`:

The Swift implementation:

```swift
import Foundation

@objc(MyModule)
class MyModule: NSObject {
  @objc func getDeviceInfo(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let info: [String: String] = [
      "model": UIDevice.current.model,
      "osVersion": UIDevice.current.systemVersion
    ]
    resolve(info)
  }

  @objc func computeHash(_ input: String) -> String {
    // synchronous — returns directly
    return input.data(using: .utf8).map {
      $0.map { String(format: "%02x", $0) }.joined()
    } ?? ""
  }
}
```

For the New Architecture, you also implement the generated protocol from CodeGen. The generated protocol name follows the pattern `NativeMyModuleSpec`. Your Swift class must conform to it.

Register the module in a separate Objective-C file to satisfy React Native's module registration system:

```objc
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(MyModule, NSObject)
  RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
                    rejecter:(RCTPromiseRejectBlock)reject)
  RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(computeHash:(NSString *)input)
@end
```

## Step 4: Android Implementation in Kotlin

On Android, implement the generated abstract class from CodeGen. The generated class name follows the pattern `NativeMyModuleSpec`:

```kotlin
package com.myapp

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Promise

class MyModule(reactContext: ReactApplicationContext) :
    NativeMyModuleSpec(reactContext) {

  override fun getName() = NAME

  override fun getDeviceInfo(promise: Promise) {
    val info = mapOf(
      "model" to android.os.Build.MODEL,
      "osVersion" to android.os.Build.VERSION.RELEASE
    )
    promise.resolve(
      com.facebook.react.bridge.Arguments.makeNativeMap(info)
    )
  }

  override fun computeHash(input: String): String {
    return input.toByteArray()
      .joinToString("") { "%02x".format(it) }
  }

  companion object {
    const val NAME = "MyModule"
  }
}
```

Register the package in your `MainApplication`:

```kotlin
packages.add(MyModulePackage())
```

## Step 5: Using the Module from JavaScript

Back in JavaScript, import from your spec file:

```ts
import NativeMyModule from './NativeMyModule';

const info = await NativeMyModule.getDeviceInfo();
console.log(info.model, info.osVersion);

const hash = NativeMyModule.computeHash('hello');
```

TypeScript will enforce the correct argument types and return types based on the spec you wrote.

## Using the Expo Modules API Instead

If the CodeGen and protocol conformance boilerplate above feels like a lot, the Expo Modules API provides a higher-level abstraction over the same JSI foundation. It lets you define your module in a declarative Kotlin/Swift DSL without writing the spec file separately or managing CodeGen configuration manually.

For modules you are shipping as open-source packages, the Expo Modules API is the better default in 2026 — it handles the New Architecture compatibility surface for you. For internal modules in a bare React Native project where you control the full build, either approach works.

## Testing Native Modules

Write your native implementation tests in the native testing framework (XCTest for iOS, JUnit for Android) to catch errors before they surface in JavaScript. On the JavaScript side, mock the native module in Jest:

```ts
jest.mock('./NativeMyModule', () => ({
  getDeviceInfo: jest.fn().mockResolvedValue({ model: 'iPhone', osVersion: '17' }),
  computeHash: jest.fn().mockReturnValue('aabbcc'),
}));
```

This keeps your JavaScript logic tests fast and independent of native builds.

---

TurboModules are straightforward once you have the pattern established. The TypeScript spec as the source of truth is a genuine improvement over the old approach — types flow from a single definition to both native implementations rather than being duplicated and able to drift.

If you need a team to build or integrate a native module — Bluetooth, hardware SDKs, payment processors, biometrics — [Clixo handles this end-to-end](https://clixo.sh/#contact).

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
