| name | build-nitro-modules |
| description | Builds and designs React Native Nitro Modules with Nitrogen, HybridObject TypeScript specs, generated native implementations, zero-copy and native-state APIs, Swift/Kotlin/C++ bindings, example apps, and testing. Use when creating a Nitro Module, adding or reviewing HybridObjects, designing Nitro-specific public APIs, implementing native functionality, or setting up the nitrogen codegen pipeline. Pair with api-design for general library API shape. |
| license | MIT |
| metadata | {"author":"margelo","tags":"react-native, nitro-modules, nitrogen, hybrid-object, api-design, swift, kotlin, c++, monorepo, native-modules, codegen"} |
Build Nitro Modules
Overview
End-to-end skill for building a React Native Nitro Module: monorepo scaffolding via Nitrogen, TypeScript HybridObject spec authoring, native code generation, platform implementation (C++/Swift/Kotlin), example app wiring, and publish preparation.
Nitro Modules use a codegen pipeline (nitrogen) that reads .nitro.ts spec files and generates native C++/Swift/Kotlin boilerplate. You then fill in the implementation. This is fundamentally different from old-style turbo modules.
Generated files under nitrogen/generated/ are outputs. Change the .nitro.ts spec or native implementation source, then re-run nitrogen instead of manually editing generated files. These files can be committed to git, and many Nitro libraries do commit them, but the repo policy can choose otherwise. They must be included in the npm package so consumers can build the native library.
Pair With API Design
Use api-design first when shaping the public TypeScript, JavaScript, React, or React Native API. This skill adds the Nitro-specific constraints: HybridObject state, generated specs, native resource ownership, zero-copy data, threading, platform implementation, codegen, and real-device validation.
Let api-design own general public API rules and API freshness checks. In this skill, only add Nitro-specific freshness checks for mobile toolchain and generated-template decisions: verify current Nitro, React Native, Gradle, Xcode, Swift, Kotlin, NDK, and package-tooling docs/source before choosing versions, config fields, or native implementation details.
If the user is building a JS-only React or React Native library, do not apply this skill unless Nitro, HybridObjects, native modules, codegen, C++/Swift/Kotlin bindings, or react-native-nitro-modules are part of the task.
Repo and Release References
Load repo-structure-and-workflow.md only when creating a repo, reorganizing layout, adding examples/docs/CI, or changing workflow policy.
Load release-it-publishing.md only when setting up or reviewing bun release / release-it.
Nitro API Design Rules
- Prefer Nitro Modules over TurboModules or handwritten JSI for native module work. Nitro is usually faster and safer because it avoids many raw JSI lifetime, threading, and runtime-destruction hazards. Use raw JSI only when Nitro's Raw JSI Methods are required.
- Keep the root HybridObject default-constructible for autolinking. Create argument-dependent objects through factory methods.
- Use HybridObjects for native state: native resources, prewarmed engines, files, images, databases, sensor sessions, streams, and other stateful objects.
- If native setup is required, make the factory method async and resolve with a ready HybridObject, such as
createCameraSession(...): Promise<CameraSession>.
- One JS-facing HybridObject spec can have multiple native concrete classes implementing the generated spec. Use this to hide backend strategies behind one TypeScript type, for example
CameraVideoOutput backed by either a movie-file output or a video-data-output plus asset writer. Factories choose the native implementation and return the shared spec type.
- Use a product/domain noun for the exported JS factory object, not the generated spec type name. For example, export
VisionCamera = createHybridObject<CameraFactory>('CameraFactory') or Images = createHybridObject<ImageFactory>('ImageFactory'). This avoids collisions with CameraFactory/ImageFactory types without mechanically lowercasing them or adding Hybrid prefixes.
- Keep each HybridObject scoped to one purpose or lifecycle.
- Treat HybridObjects as primary API objects. Each primary HybridObject gets its own
.nitro.ts file.
- Keep an inheritance family in one
.nitro.ts file only when the file is named after the base HybridObject and child HybridObjects add few or no members, such as ScannedCode, ScannedBarcode, and ScannedQRCode in ScannedCode.nitro.ts.
- Put named codegen types in their own
.ts files: string-literal unions/enums, structs/interfaces, option objects, event objects, callback option structs, and helper types. Nitro needs names for generated native structs and enum-like values. Import them into .nitro.ts specs and re-export public types from src/index.ts.
- Inline simple function callbacks in method signatures, for example
addErrorListener(listener: (error: Error) => void): ListenerSubscription. Do not create one-off aliases such as ScannerErrorListener unless the function type is reused as a public concept across multiple APIs.
- Group multiple helper types in one file only when they form one tightly coupled logical construct, such as
DynamicRange plus the exact literal unions that define it.
- Use HybridObject inheritance for shared native state plus specialized result shapes. Put shared properties such as IDs, bounds, raw values, formats, and value types on the base object instead of repeating them on every subtype.
- Use HybridObject inheritance for heterogeneous native result families. Example:
ScannedItem owns common state and methods, while ScannedBarcode, ScannedQRCode, and ScannedFace extend it with specialized properties. APIs can return ScannedItem[]; JS narrows by a discriminator property, and native code can accept the generated base spec when it only needs common behavior.
- Autolink only public roots, factories, views, or global utilities that JS must construct directly. Other HybridObjects can be returned from factory methods and do not need their own
nitro.json autolinking entries. Do not autolink every concrete native implementation of the same JS-facing spec.
- For native extension points, pair a JS-facing base HybridObject spec with a public native protocol/interface. The base spec lets JS pass the object through typed APIs; the native protocol/interface exposes platform-specific handles and behavior for first-party and third-party native code.
- When accepting an extensible HybridObject from JS, accept the generated base spec type, then cast to the native protocol/interface on the native side and throw a clear error if it does not conform. This keeps JS portable while native integrations stay strongly typed.
- Use Nitro structs for domain shapes, option groups, and same-type parameter clusters. Do not wrap unrelated hot-path values in a struct only to reduce argument count; Nitro eagerly converts structs, so unnecessary wrappers can be slower than explicit parameters.
- Do not model high-volume native results, parsed payloads, images, buffers, or objects with many optional expensive fields as flat structs. Nitro structs are eagerly converted, so prefer stateful HybridObjects with lazy properties or methods for data the caller may never read.
- Use
ArrayBuffer for small or truly zero-copy native data access. For large media, photos, scans, model outputs, or byte payloads, return a HybridObject and expose lazy methods such as toArrayBuffer(), toBase64(), or saveToTemporaryFile() instead of eagerly converting bytes into JS.
- Keep raw native state behind HybridObjects when conversion cost matters. For example, a native barcode/photo/result object can expose
rawValue, bounds, format, or byte data lazily instead of converting every field for every detection.
- Listener subscriptions should be flat structs with a
remove: () => void function field, not HybridObjects, unless they expose native state beyond cleanup. Call sites still use subscription.remove().
- Use
setOn...Callback(callback | undefined) only for single hot-path callbacks owned by an object, where replacing or removing the callback is the natural operation.
- Use
Sync<(...) => ...> callbacks only for rare thread-bound hot paths that must synchronously execute on a specific JS runtime or worklet thread.
- For Nitro Views, expose the raw
getHostComponent wrapper. Add React components or hooks only when they remove repeated setup code while staying layered over the same native objects and refs.
- Follow
api-design for naming, platform abstraction, sync/async boundaries, listener cleanup, errors, variants, TypeScript facades, and JSDoc contracts.
Nitro Native Implementation Rules
- Check Nitro's current minimum requirements before debugging weird build failures. Current Nitro docs list React Native 0.75+, Xcode 16.4+, Swift 5.9+, Android
compileSdkVersion 34+, and NDK 27+; Nitro Views currently require React Native 0.78+ and the New Architecture.
- Every native HybridObject implementation must implement or inherit from the generated
Hybrid*Spec for that platform. Never implement a standalone native class and expect Nitro to discover it.
- Make native implementation classes
final by default unless inheritance is genuinely required. This is especially true for Swift and Kotlin HybridObjects.
- Keep top-level native types in separate files. Nest only truly local private helpers.
- Validate invalid inputs and required unsupported behavior early. Do not reject cross-platform configuration just because the current native backend ignores an optional preference. If the operation still does its core job, ignore or degrade the preference and report support through capabilities or resolved state.
- Do not silently swallow real failures. Throw, reject a Promise, or emit through an explicit error callback/listener when the requested outcome cannot be delivered.
- Prefer Nitro/runtime errors or language-native exceptions that surface cleanly to JS. Avoid Objective-C-style
NSError public paths unless the generated API specifically requires it.
- For Android context access, use
NitroModules.applicationContext lazily and throw a clear error if it is unavailable.
- Use the native Promise helper that matches the platform threading model. In Swift, prefer
Promise.parallel(queue) for DispatchQueue-based work such as AVFoundation/session operations, and use Promise.async only when wrapping Swift async/await or Task-based APIs.
- Implement
memorySize for HybridObjects that own native resources or large allocations so the JS VM can collect them under memory pressure.
- For Nitro Views, implement
prepareForRecycle when the view owns state that should be reset before reuse.
- Mix C++ HybridObjects with Swift/Kotlin HybridObjects in one library. Use C++ for shared or hot code, such as OpenCV/frame processing/storage engines, and Swift/Kotlin for platform services, permissions, file paths, camera/session APIs, and OS integration.
- C++ HybridObjects can accept Swift/Kotlin-implemented HybridObjects and call their generated C++ spec API. Example: a C++
StorageFactory can accept a Swift/Kotlin PlatformContext and call getTemporaryDirectory() or writeFile(...) through the generated C++ interface. C++ can access only the public spec API, not private Swift/Kotlin fields.
- Do not rely on Swift/Kotlin calling into C++-implemented HybridObjects unless current Nitrogen support has been verified for that direction.
Nitro Testing Rules
- Prefer behavior tests over type-shape tests. Nitrogen already enforces specs at compile time, so tests should cover real feature behavior, inputs, settings, failure paths, and order-of-execution cases.
- Use
react-native-harness when available for end-to-end testing in a real React Native environment. For native-heavy libraries, prefer real-device or CI device-farm coverage for the API surface that depends on hardware or OS behavior.
Ask First — Before Doing Anything
First, determine what the user wants to do:
"Are you creating a new Nitro Module library from scratch, or adding a new HybridObject to an existing library?"
If creating a new library — ask all of these before any command:
- Library name — What should the library be called? (e.g.
react-native-math)
- Monorepo with
packages/ folder — Should the library live in packages/<name> inside a monorepo? (Strongly recommended — default: yes)
- Example app — Should an example app be created to test the module, and where should it live? (Recommended — default: yes;
apps/example when multiple examples are needed or likely, example only for a small single-example repo that should stay close to generated RN config)
- Native languages — Which platforms and languages?
- iOS:
swift (default) or cpp
- Android:
kotlin (default) or cpp
- Cross-platform C++ only: both
cpp
- Module purpose — Briefly describe what the module does so the correct spec methods can be designed
Do not proceed past Step 1 of the build sequence until all five questions are answered.
If adding a HybridObject to an existing library — ask only:
- HybridObject name — What should the new HybridObject be called? (e.g.
Camera, Crypto)
- Native languages — iOS:
swift or cpp? Android: kotlin or cpp?
- Purpose — What does this HybridObject do?
Then skip directly to spec-hybrid-object.md (write the spec), spec-nitro-json.md (add autolinking entry), native-nitrogen-codegen.md (re-run nitrogen), and the relevant native implementation file. Skip all setup, monorepo, and example app steps.
Typical Build Sequence
bunx nitrogen@latest init react-native-math
cd packages/react-native-math && bunx nitrogen
bunx @react-native-community/cli@latest init --skip-install MathExample
mkdir -p apps && mv MathExample apps/example
cd apps/example && bun add ../../packages/react-native-math
bun add react-native-nitro-modules@<same-version-as-package>
bun example android
bun example ios
Full step-by-step references below.
When to Apply
Reference these guidelines when:
- Creating any new React Native native module using the Nitro framework
- Checking Nitro minimum platform requirements
- Verifying current Nitro, React Native, and native-toolchain requirements before making implementation decisions
- Designing or reviewing the public API shape of a Nitro-backed library
- Deciding whether an API should be static, instance-based, sync, async, callback-based, or resource-backed
- Writing HybridObject TypeScript specs (
*.nitro.ts files)
- Running Nitrogen codegen and implementing generated interfaces
- Setting up a monorepo example app for a Nitro library
- Choosing repository layout, root cleanliness, shared config placement, and CI shape
- Establishing branch, draft PR, and squash-merge workflow for a Nitro library
- Configuring Android Gradle paths for a monorepo structure
- Debugging autolinking failures or missing generated files
- Preparing a Nitro module package for npm publishing
- Setting up one-command releases with
release-it and bun release
Priority-Ordered Guidelines
Quick Reference
Minimum HybridObject Spec (src/specs/Math.nitro.ts)
import type { HybridObject } from 'react-native-nitro-modules'
export interface Math extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
add(a: number, b: number): number
}
Minimum Runtime + Type Exports (src/index.ts)
import { NitroModules } from 'react-native-nitro-modules'
import type { Math } from './specs/Math.nitro'
export const math = NitroModules.createHybridObject<Math>('Math')
export type { Math } from './specs/Math.nitro'
Minimum nitro.json
{
"$schema": "https://nitro.margelo.com/nitro.schema.json",
"cxxNamespace": ["math"],
"ios": { "iosModuleName": "NitroMath" },
"android": {
"androidNamespace": ["math"],
"androidCxxLibName": "NitroMath"
},
"autolinking": {
"Math": {
"ios": {
"language": "swift",
"implementationClassName": "HybridMath"
},
"android": {
"language": "kotlin",
"implementationClassName": "HybridMath"
}
}
}
}
Root package.json Scripts
{
"scripts": {
"specs": "bun --cwd packages/react-native-math run specs",
"example": "bun --cwd apps/example"
}
}
Run: bun example android, bun example ios, bun specs
References
| File | Description |
|---|
| repo-structure-and-workflow.md | Root layout, packages/apps/docs/config/scripts, CI, branch, draft PR, and squash-merge workflow |
| setup-monorepo-init.md | Collecting scaffold inputs and running nitrogen init |
| spec-hybrid-object.md | Writing *.nitro.ts specs and exporting HybridObjects |
| spec-nitro-json.md | nitro.json all fields, autolinking, namespace configuration |
| native-nitrogen-codegen.md | Running Nitrogen and verifying generated files |
| native-implement-cpp.md | Implementing HybridObjects in C++ |
| native-implement-kotlin.md | Implementing HybridObjects in Kotlin (Android) |
| native-implement-swift.md | Implementing HybridObjects in Swift (iOS) |
| example-app-setup.md | RN CLI example app init, workspace wiring, version alignment |
| example-android-config.md | settings.gradle and build.gradle monorepo path fixes |
| example-metro-install.md | Metro watchFolders, library install, App.tsx usage, test runs |
| spec-package-publish.md | package.json author, files field, and npm publish readiness |
| release-it-publishing.md | One-command releases with release-it and bun release |
| vision-camera-golden-standard.md | Package layout, API layering, Nitro object modeling, and publishing patterns inspired by VisionCamera |
Problem → Skill Mapping
| Problem | Reference | Action |
|---|
| Need to design the public API first | api-design + this SKILL.md | Shape the TS/React API, then apply Nitro constraints |
| Need latest general APIs | api-design | Check official docs, release notes, source repos, package metadata, or llms-full.txt before deciding |
| Need a recommended repo structure | repo-structure-and-workflow.md | Use packages/, apps/ or example/, optional docs/, scripts/, config/, and .github/workflows/ |
| Unsure static module vs instance API | This SKILL.md | Prefer HybridObjects for native state, resources, prewarming, and zero-copy data |
| Don't know where to start | setup-monorepo-init.md | Scaffold with nitrogen init |
| Spec file syntax error | spec-hybrid-object.md | Fix *.nitro.ts interface |
| Autolinking not working | spec-nitro-json.md | Check nitro.json autolinking block |
| Nitrogen generates no files | native-nitrogen-codegen.md | Verify spec file extension and run command from right dir |
| C++ types unclear | native-implement-cpp.md | Follow type reference links to canonical examples |
| Kotlin compilation error | native-implement-kotlin.md | Check annotations and override modifiers |
| Swift compilation error | native-implement-swift.md | Check class inheritance and property signatures |
| Example app won't build (Android) | example-android-config.md | Fix Gradle monorepo path configuration |
| Metro can't resolve library | example-metro-install.md | Add watchFolders to metro.config.js |
| Version mismatch between example and package | example-app-setup.md | Align react-native versions across workspaces |
| Package missing files on npm | spec-package-publish.md | Fix files field in package.json |
| Need one-command releases | release-it-publishing.md | Configure release-it behind bun release |
| Need a full-featured library structure | vision-camera-golden-standard.md | Use the VisionCamera-inspired package, API, hooks, views, and Nitro object model |