Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions skills/build-nitro-modules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,17 @@ Reference these guidelines when:
| Priority | Category | Impact | Reference |
|----------|----------|--------|-----------|
| 1 | Monorepo scaffold | CRITICAL | [setup-monorepo-init.md][setup-monorepo-init] |
| 2 | HybridObject spec | CRITICAL | [spec-hybrid-object.md][spec-hybrid-object] |
| 3 | nitro.json autolinking | CRITICAL | [spec-nitro-json.md][spec-nitro-json] |
| 4 | Nitrogen codegen | HIGH | [native-nitrogen-codegen.md][native-nitrogen-codegen] |
| 5 | C++ implementation | HIGH | [native-implement-cpp.md][native-implement-cpp] |
| 6 | Kotlin implementation | HIGH | [native-implement-kotlin.md][native-implement-kotlin] |
| 7 | Swift implementation | HIGH | [native-implement-swift.md][native-implement-swift] |
| 8 | Example app setup *(if requested)* | HIGH | [example-app-setup.md][example-app-setup] |
| 9 | Android Gradle paths *(if example app)* | HIGH | [example-android-config.md][example-android-config] |
| 10 | Metro + install + test *(if example app)* | HIGH | [example-metro-install.md][example-metro-install] |
| 11 | npm publish prep | MEDIUM | [spec-package-publish.md][spec-package-publish] |
| 2 | API design best practices | CRITICAL | [api-design-best-practices.md][api-design-best-practices] |
| 3 | HybridObject spec | CRITICAL | [spec-hybrid-object.md][spec-hybrid-object] |
| 4 | nitro.json autolinking | CRITICAL | [spec-nitro-json.md][spec-nitro-json] |
| 5 | Nitrogen codegen | HIGH | [native-nitrogen-codegen.md][native-nitrogen-codegen] |
| 6 | C++ implementation | HIGH | [native-implement-cpp.md][native-implement-cpp] |
| 7 | Kotlin implementation | HIGH | [native-implement-kotlin.md][native-implement-kotlin] |
| 8 | Swift implementation | HIGH | [native-implement-swift.md][native-implement-swift] |
| 9 | Example app setup *(if requested)* | HIGH | [example-app-setup.md][example-app-setup] |
| 10 | Android Gradle paths *(if example app)* | HIGH | [example-android-config.md][example-android-config] |
| 11 | Metro + install + test *(if example app)* | HIGH | [example-metro-install.md][example-metro-install] |
| 12 | npm publish prep | MEDIUM | [spec-package-publish.md][spec-package-publish] |

## Quick Reference

Expand Down Expand Up @@ -144,6 +145,7 @@ Run: `bun example android`, `bun example ios`, `bun specs`
| File | Description |
|------|-------------|
| [setup-monorepo-init.md][setup-monorepo-init] | Monorepo workspace structure and `nitrogen init` scaffold |
| [api-design-best-practices.md][api-design-best-practices] | Nitro API shape, typed specs, errors, native state, memory, buffers, hooks, and Harness tests |
| [spec-hybrid-object.md][spec-hybrid-object] | Writing `*.nitro.ts` specs and exporting HybridObjects |
| [spec-nitro-json.md][spec-nitro-json] | `nitro.json` all fields, autolinking, namespace configuration |
| [native-nitrogen-codegen.md][native-nitrogen-codegen] | Running Nitrogen and verifying generated files |
Expand All @@ -160,6 +162,7 @@ Run: `bun example android`, `bun example ios`, `bun specs`
| Problem | Reference | Action |
|---------|-----------|--------|
| Don't know where to start | [setup-monorepo-init.md][setup-monorepo-init] | Scaffold with `nitrogen init` |
| API shape unclear | [api-design-best-practices.md][api-design-best-practices] | Prefer typed, instance-based APIs with explicit errors |
| Spec file syntax error | [spec-hybrid-object.md][spec-hybrid-object] | Fix `*.nitro.ts` interface |
| Autolinking not working | [spec-nitro-json.md][spec-nitro-json] | Check `nitro.json` autolinking block |
| Nitrogen generates no files | [native-nitrogen-codegen.md][native-nitrogen-codegen] | Verify spec file extension and run command from right dir |
Expand All @@ -172,6 +175,7 @@ Run: `bun example android`, `bun example ios`, `bun specs`
| Package missing files on npm | [spec-package-publish.md][spec-package-publish] | Fix `files` field in `package.json` |

[setup-monorepo-init]: references/setup-monorepo-init.md
[api-design-best-practices]: references/api-design-best-practices.md
[spec-hybrid-object]: references/spec-hybrid-object.md
[spec-nitro-json]: references/spec-nitro-json.md
[native-nitrogen-codegen]: references/native-nitrogen-codegen.md
Expand Down
103 changes: 103 additions & 0 deletions skills/build-nitro-modules/references/api-design-best-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Nitro API Design Best Practices
impact: CRITICAL
tags: api-design, best-practices, hybrid-object, types, errors, performance, harness
---

# Skill: Nitro API Design Best Practices

Use this before writing the `.nitro.ts` spec and keep using it while implementing Swift/Kotlin/C++. Nitro benefits most from explicit, typed, instance-based APIs that expose native state safely instead of copying everything through JS.

## Hard Rules

- **Do not inherit from `_base`.** Implement the generated spec directly (`HybridFooSpec`) or the real Nitro base type required by the current API. Generated `_base` types are implementation details and should not be part of library code.
- **Don't create functions that could be simple property getters.** For example, use `readonly isAccelerometerAvailable: boolean` instead of `isAccelerometerAvailable(): boolean` when the value is state-like and cheap to read.
- **Use `is*`, `has*`, or similar prefixes for boolean properties.** Bad: `accelerometerAvailable`. Good: `isAccelerometerAvailable`.
- **Use objects / structs instead of 3+ params.** If a method has three or more related arguments, define a typed options/result struct in the `.nitro.ts` spec.
- **Always prefer typed structs/methods/types over `AnyMap` or `Record<K, V>`.** Nitro benefits from statically typed bindings for better performance. Variants (`A | B`) also require runtime overhead, but those are fine; if they can be avoided it is good, but otherwise its no biggie.
- **Use Nitro's recommended types for TypeScript specs.** Prefer primitives (`number`, `string`, `UInt64`, `boolean`), `ArrayBuffer`, `Promise`, typed structs, and callbacks. Use `Error` instead of custom typed errors, as only those are true JS `Error` prototypes.
- **Do not create multiple types in a single file at top level.** Create multiple files for stuff like this. The only exception is truly local types/structs/classes/enums, which can be nested private.
- **Make almost all classes `final`.** This is better for performance, unless you really expect them to be overridden. Usually that never happens in Nitro HybridObjects.

## Cross-Platform API Abstractions

React Native is a cross-platform framework - try to avoid leaking too much platform specific information into public APIs. For example, instead of 1:1 mirroring iOS/Android APIs to TypeScript, try to find common abstractions.

Try not to overly abstract like the Web/JS-style. The sweet-spot is right in-between. See `react-native-vision-camera` or `react-native-nitro-image` for good, cross-platform abstractions that don't leak platform specific behaviour into user APIs.

Only rarely, e.g. like the `CameraObjectOutput` which is a native Object Metadata output implemented via `AVCaptureMetadataObjectsOutput` - this is only available on iOS, yet the public APIs don't mention the AVFoundation types as it is unnecessary for the user.

On the other side, stuff like GPU-powered, zero-copy, or similar concepts are close-to-the-metal APIs that are good to expose to the API user via Nitro, which is something Web-APIs often avoid. Find the sweet-spot here.

## Error Handling

- **Avoid swallowing errors silently.** Don't just early return, don't just log/print. Either throw or reject promise if possible, or have a separate error listener if an error is being sent in a different execution context.
- Do not write guards like `guard motionManager.isAccelerometerAvailable else { return }`. Throw a Nitro runtime error or reject the promise so JS can handle the failure.
- **Avoid silently swallowing not implemented functions.** If a feature is not available on a platform, throw an explicit not-available/not-implemented error.
- Do not use `NSError` or Objective-C types in general. Prefer Nitro's runtime error type, for example Swift's `RuntimeError("...")` from `NitroModules`.
- For impossible-to-reach states, you can use static assertions or `fatalError(...)` in Swift, but this should be super rare and not for stuff that can be reached via faulty JS user code.

```swift
import NitroModules

final class HybridMotion: HybridMotionSpec {
var isAccelerometerAvailable: Bool {
return motionManager.isAccelerometerAvailable
}

func startAccelerometerUpdates() throws {
guard motionManager.isAccelerometerAvailable else {
throw RuntimeError("Accelerometer is not available on this device.")
}
motionManager.startAccelerometerUpdates()
}
}
```

## Native State and Object Shape

- **Nitro allows us to use native state (`HybridObject`).** This is especially useful to zero-copy bridge native data, for example large `UIImage`, zero-copy access via `ArrayBuffer`, or properties/methods on it.
- Use instance-based APIs and native state API design patterns. For haptics modules, instead of making everything a static method as you would have in a TurboModule, make the haptics engine an instance (`HybridObject`) and pre-warm the engine so that a call to trigger can be faster.
- For HybridObjects that require arguments to be constructed, use a factory pattern. For example, for a Nitro `File` object that needs to be created from a path string, create `FileFactory` with a method like `loadFileFromPath(path: string): Promise<File>`. Here, `Promise` is important because it is async. For non-async/fast methods use sync APIs.
- Use sync methods by default as that is super fast, but for anything that takes longer to execute, for example hardware calls or async APIs, use async methods (`Promise<...>`). In native code, use `Promise.parallel` for dispatch queue/thread based work or `Promise.async` for async/coroutine based work depending on the native threading requirements.
- Use descriptive method names. For helpers, prefer names like `timestampMs()` over vague names like `timestamp()`.

```swift
private static func timestampMs() -> Double {
return Date().timeIntervalSince1970 * 1_000.0
}
```

## Memory, Views, and Buffers

- For types that hold onto native resources or have large memory allocations, implement `memorySize` (an overridable property in `HybridObject`) and try to estimate the size somewhat closely. For example, estimate a `UIImage` byte size by multiplying width, height, and bytes per pixel. This helps the JS VM / GC delete sooner and avoid memory stress.
- For Nitro Views, implement view recycling if applicable. Use `prepareForRecycle`, which is overridable from `HybridView`.
- For zero-copy data access, use `ArrayBuffer`; it can wrap many native buffer types.
- When receiving an `ArrayBuffer` from JS but you need to use it on a different thread, check if it is owning or not via `arrayBuffer.isOwner`. If not, copy it first: `let buffer = arrayBuffer.isOwner ? arrayBuffer : ArrayBuffer.copy(of: arrayBuffer)`.

## Platform Interop

- If you need Android context, use `NitroModules.applicationContext` and just throw if it is not available.
- If you need to mix C++ and platform languages (Swift/Kotlin), you can do so in Nitro. This allows re-using C++ code across iOS and Android.
- You can pass a HybridObject implemented in a platform language (for example Swift/Kotlin) to C++ and use it as normal in C++. This is useful for something like SQLite, where a `PlatformFilesystem` HybridObject has a `createFile(): string` method. JS can create `PlatformFilesystem` and pass it to a C++ SQLite HybridObject, and C++ can call `createFile()` directly even though it is implemented in Swift/Kotlin.
- Prefer Nitro Modules over JSI. Nitro is not only faster than TurboModules and often even faster than handwritten JSI, but also much safer: it avoids retaining JSI values across different threads, calling Promise resolve after runtime destruction, and similar crash-prone patterns.
- Resort to JSI only if absolutely needed via Raw JSI Methods (`prototype.registerRawHybridMethod` API from Nitro C++).

## TypeScript Layer

- In React Native environments, try to provide Hooks APIs on top of the imperative APIs. Use an initial getter (`sync` in `useMemo`/`useRef`/`useSyncExternalStore`, async via `useEffect` or similar) plus listener-based APIs with an unsubscribe function via `useEffect`.
- When creating TypeScript abstractions on top of native Nitro bindings, you can use TypeScript features like discriminating unions or nullables with default values more easily. Those things would have slight overhead and more type complexity in native Nitro code.
- Example: `takePhoto(options:)` might have a complex options struct. Making all of that optional causes more complex native code; simply making it non optional and providing default values via TypeScript (`??`) makes it simpler, easier to inline for the JS engine, and lower bridging cost.
- The goal should always be to provide simpler and more explicit native APIs. Don't overengineer this; if it is overcomplicating native code then it is not worth the minor performance gain.
- Use callbacks for asynchronous functions, and in super rare cases when a callback needs to run fully synchronously/blocking on the JS Thread, use `Sync<(...) => ...>`, for example for worklets interop. See `react-native-vision-camera` V5 `FrameOutput.nitro.ts` for an example on how to use `Sync` callbacks on Worklet Threads.

## Testing

- If available, use `react-native-harness` for end-to-end testing Nitro modules.
- Write Harness tests for real features to ensure that each individual feature, input/param combination, setting, property, combination, order of execution, and more works properly in a real React Native environment.
- Use Harness CI tests via GitHub Actions to continuously iterate until CI is green and to cover more API surface area / combinations.
- Test behaviour. There is no point in testing types like `toBeDefined`, `Array.isArray`, or `typeof`, because Nitro(gen) already enforces types at compile-time.

## When Unsure

Refer to Nitro's docs if anything else is unclear: <https://nitro.margelo.com/llms-full.txt>
4 changes: 4 additions & 0 deletions skills/build-nitro-modules/references/native-implement-cpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,13 @@ void HybridMath::compute(double input, std::function<void(double)> onResult) {
- **Using `float` instead of `double`** — Nitro uses `double` for all `number` types
- **Using `std::future<T>`** — Nitro does not use `std::future`; always use `std::shared_ptr<Promise<T>>` with `Promise<T>::async(...)`
- **Missing `TAG` member** — Required for `HybridObject(TAG)` constructor call
- **Inheriting from generated `_base` types** — Do not inherit from `_base`; implement the generated spec directly or the real Nitro base type required by the current API
- **Silently returning on unavailable APIs** — Throw a clear exception instead of returning, logging, or printing
- **Generic maps by default** — Prefer typed structs/methods/types over `AnyMap` or `Record<K, V>`

## Related Skills

- [api-design-best-practices.md](api-design-best-practices.md) — API shape, errors, native state, memory, buffers, hooks, and Harness tests
- [native-nitrogen-codegen.md](native-nitrogen-codegen.md) — Must generate specs before implementing
- [spec-nitro-json.md](spec-nitro-json.md) — Configure `"c++"` in autolinking
- [native-implement-kotlin.md](native-implement-kotlin.md) — Android Kotlin alternative
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class HybridMath : HybridMathSpec() {
```kotlin
@Keep
@DoNotStrip
class HybridMath : HybridMathSpec() {
final class HybridMath : HybridMathSpec() {
override fun add(a: Double, b: Double): Double = a + b
}
```
Expand Down Expand Up @@ -65,7 +65,7 @@ import com.facebook.proguard.annotations.DoNotStrip

@Keep
@DoNotStrip
class HybridMath : HybridMathSpec() {
final class HybridMath : HybridMathSpec() {

// Synchronous method
override fun add(a: Double, b: Double): Double = a + b
Expand Down Expand Up @@ -228,9 +228,12 @@ override fun divide(a: Double, b: Double): Double {
- **Calling blocking code outside `Promise.async`** — Network calls, delay, etc. must be inside `Promise.async { }` (uses coroutines)
- **Storing `NitroModules.applicationContext` in a field** — It can be null at construction time; always access it via a `get()` property
- **Not null-checking `applicationContext`** — Always use `?: throw Error("No ApplicationContext set!")` to fail explicitly
- **Silently returning on unavailable APIs** — Throw a clear error instead of returning, logging, or printing
- **Non-final implementation classes** — Make almost all classes `final` unless you really expect subclasses

## Related Skills

- [api-design-best-practices.md](api-design-best-practices.md) — API shape, errors, native state, memory, buffers, hooks, and Harness tests
- [native-nitrogen-codegen.md](native-nitrogen-codegen.md) — Must generate specs before implementing
- [spec-nitro-json.md](spec-nitro-json.md) — Configure `"kotlin"` in autolinking
- [native-implement-swift.md](native-implement-swift.md) — iOS Swift counterpart
Expand Down
10 changes: 7 additions & 3 deletions skills/build-nitro-modules/references/native-implement-swift.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class HybridMath: NSObject {
```swift
import NitroModules

class HybridMath: HybridMathSpec {
final class HybridMath: HybridMathSpec {
func add(a: Double, b: Double) throws -> Double { a + b }
}
```
Expand Down Expand Up @@ -58,7 +58,7 @@ touch ios/HybridMath.swift
```swift
import NitroModules

class HybridMath: HybridMathSpec {
final class HybridMath: HybridMathSpec {

// Synchronous methods — most generated methods have `throws`
func add(a: Double, b: Double) throws -> Double {
Expand Down Expand Up @@ -182,7 +182,7 @@ func round(value: Double, decimals: Double?) -> Double {
```swift
func divide(a: Double, b: Double) throws -> Double {
guard b != 0 else {
throw NSError(domain: "Math", code: 1, userInfo: [NSLocalizedDescriptionKey: "Division by zero!"])
throw RuntimeError("Division by zero!")
}
return a / b
}
Expand Down Expand Up @@ -214,9 +214,13 @@ var zoom: Double {
- **`any HybridSpec` not `HybridSpec`** — In modern Swift, protocol types need the `any` keyword
- **Not including the file in podspec** — Swift files must be in the `source_files` glob in `.podspec`
- **Using the `override` keyword** — The generated spec is a Swift *protocol*, not a superclass. Conforming methods and properties must NOT use `override` (unlike the Kotlin counterpart, which does). `override` only applies when overriding a superclass member.
- **Using `NSError` or Objective-C types** — Nitro bridges Swift directly; prefer `RuntimeError("...")` or another Swift `Error`
- **Silently returning on unavailable APIs** — Throw a clear runtime error instead of `guard ... else { return }`
- **Non-final implementation classes** — Make almost all classes `final` unless you really expect subclasses

## Related Skills

- [api-design-best-practices.md](api-design-best-practices.md) — API shape, errors, native state, memory, buffers, hooks, and Harness tests
- [native-nitrogen-codegen.md](native-nitrogen-codegen.md) — Must generate specs before implementing
- [spec-nitro-json.md](spec-nitro-json.md) — Configure `"swift"` in autolinking
- [native-implement-kotlin.md](native-implement-kotlin.md) — Android Kotlin counterpart
Expand Down
Loading