nx-grpc-kit
v0.4.0
Published
Nx plugin for gRPC proto generation + composable gRPC-web interceptors for Angular
Maintainers
Readme
nx-grpc-kit
Composable gRPC-web interceptors for Angular + Nx executor for proto generation.
13 interceptor factories. Zero @Injectable() boilerplate. Plugin-agnostic proto codegen with Nx caching. Zero system dependencies — protoc and codegen plugins auto-install from npm.
Install
npm install nx-grpc-kitPeer dependencies: @angular/core >=17, @ngx-grpc/core >=3, @ngx-grpc/common >=3, rxjs >=7
Quick start
import {
provideGrpcInterceptors,
withFirebaseAuth,
withRetry,
withDeadline,
withDevTools,
withToast,
} from 'nx-grpc-kit/interceptors';
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(GrpcCoreModule.forRoot()),
importProvidersFrom(
GrpcWebClientModule.forRoot({ settings: { host: environment.grpcURL } }),
),
...provideGrpcInterceptors(
withFirebaseAuth({
getToken: () => getAuth().currentUser?.getIdToken() ?? Promise.resolve(null),
getMetadata: () => ({
orgid: authStore.selectedOrgId(),
propertyid: authStore.selectedPropId(),
}),
}),
withRetry({ maxRetries: 3 }),
withDeadline(15_000),
withDevTools({ enabled: !environment.production }),
withToast({ handler: (err) => toastService.showError(err.message) }),
),
],
};Interceptors
All interceptors are closure-based factories that return GrpcInterceptor objects. No classes, no decorators. Register them all in one provideGrpcInterceptors() call.
Core interceptors
| Factory | Purpose |
|---------|---------|
| withAuth(tokenFn) | JWT bearer token injection. Framework-agnostic — works with Firebase, Auth0, Keycloak. |
| withMetadata(metadataFn) | Custom key-value metadata per request (org context, correlation IDs). Sync or async. |
| withRetry(config?) | Exponential backoff + jitter on transient errors. Configurable status codes, max retries, delays. |
| withDeadline(ms) | Request timeout. Emits DEADLINE_EXCEEDED if the server doesn't respond in time. |
| withLogging(options?) | Request/response logging with path, timing, and status. Configurable verbosity and custom logger. |
| withErrorMapping(mapFn) | Transform gRPC status codes to application-specific errors (e.g. UNAUTHENTICATED → redirect to login). |
Presets
| Factory | Purpose |
|---------|---------|
| withFirebaseAuth(config) | Combined JWT + org/property metadata in one call. Replaces a typical 49-line @Injectable() AuthInterceptor class. |
Resilience
| Factory | Purpose |
|---------|---------|
| withCircuitBreaker(config?) | Per-service failure tracking. Trips open after N failures, cooldown with half-open probe, onStateChange callback. |
| withCache(config?) | Response caching for idempotent unary calls (List/Get/Search/Find). Configurable TTL, max entries, method whitelist with wildcards. |
| withStaleWhileRevalidate(config?) | Return cached response immediately + background refresh when stale. onRevalidate callback for store updates. |
| withOfflineQueue(config?) | Queue failed requests on network issues. Auto-retry on connectivity restore (navigator.onLine). Configurable TTL, max queue, skip patterns for read methods. Designed for mobile apps on flaky WiFi. |
DX & observability
| Factory | Purpose |
|---------|---------|
| withDevTools(config?) | Browser console table for all gRPC traffic. Minimal/normal/verbose modes. Optional window.__GRPC_DEVTOOLS__ for programmatic access. Zero overhead when disabled. |
| withToast(config) | Auto-show error toasts via generic callback. Severity mapping per status code, method-level suppression, default human-readable messages. Errors still propagate — side-effect only. |
Retry config
withRetry({
maxRetries: 3, // default: 3
initialDelayMs: 100, // default: 100
maxDelayMs: 5000, // default: 5000
backoffMultiplier: 2, // default: 2
retryableStatuses: [ // default: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED
GrpcStatusCode.UNAVAILABLE,
GrpcStatusCode.DEADLINE_EXCEEDED,
],
})Circuit breaker config
withCircuitBreaker({
failureThreshold: 5, // failures before circuit trips open
cooldownMs: 30_000, // time before allowing a probe request
failureStatuses: [GrpcStatusCode.UNAVAILABLE, GrpcStatusCode.INTERNAL],
onStateChange: (state, service) => console.log(`${service}: ${state}`),
})Offline queue config
withOfflineQueue({
maxQueueSize: 50,
requestTtlMs: 300_000, // 5 minutes — don't retry stale mutations
skipMethods: ['List*', 'Get*'], // don't queue reads
onStateChange: (event) => {
// event.type: 'queued' | 'retrying' | 'drained' | 'dropped' | 'failed'
console.log(`[offline] ${event.type}: ${event.method}`);
},
})Toast config
withToast({
handler: (error) => toastService.showError(error.message),
severityMap: {
[GrpcStatusCode.UNAUTHENTICATED]: 'suppress', // handle via redirect, not toast
[GrpcStatusCode.NOT_FOUND]: 'warn',
[GrpcStatusCode.INTERNAL]: 'error',
},
suppressMethods: ['*Polling*', 'HealthCheck'],
})DevTools config
withDevTools({
enabled: !environment.production,
verbosity: 'normal', // 'minimal' | 'normal' | 'verbose'
exposeGlobal: true, // window.__GRPC_DEVTOOLS__.calls / .errors / .clear()
})Stale-while-revalidate config
withStaleWhileRevalidate({
ttlMs: 60_000,
maxEntries: 100,
methods: ['List*', 'Get*'],
onRevalidate: (method, freshData) => {
// Update your store with fresh data in the background
},
})Utils
import {
isGrpcError,
toPlainObject,
grpcStatusToHttp,
GrpcStatusCode,
grpcStatusName,
} from 'nx-grpc-kit/utils';| Function | Purpose |
|----------|---------|
| isGrpcError(err) | Type guard — narrows unknown to GrpcStatusEvent |
| toPlainObject(msg) | Type-safe .toObject() wrapper for proto messages |
| grpcStatusToHttp(code) | Maps gRPC status code to HTTP status code |
| GrpcStatusCode | Enum with all 17 canonical gRPC status codes |
| grpcStatusName(code) | Human-readable name for a status code |
Nx Executor: proto-gen
Codegen-agnostic protoc runner with Nx caching. Zero system dependencies — protoc and codegen plugins auto-install from npm on first run.
Zero-config dependency resolution
nx run protos:proto-gen
│
├─ 1. Check node_modules/protoc/ ← npm protoc package (cross-platform)
├─ 2. Check system PATH ← fallback
├─ 3. Auto-install `protoc` from npm ← last resort
│
├─ 1. Check node_modules/.bin/ ← codegen plugin
├─ 2. Check system PATH ← fallback
├─ 3. Auto-install plugin from npm ← last resort
│
└─ Run protoc → post-process → donenpm install + nx run protos:proto-gen works on any machine — macOS x86/ARM, Linux x86/ARM, Windows x64.
Supported plugins
| Plugin name | npm package | Auto-installs |
|-------------|-------------|---------------|
| protoc-gen-ng (default) | @ngx-grpc/protoc-gen-ng | Yes |
| ts-proto | ts-proto | Yes |
| protobuf-ts | @protobuf-ts/plugin | Yes |
| Custom path | any protoc plugin | No |
project.json examples
@ngx-grpc (Angular):
{
"targets": {
"proto-gen": {
"executor": "nx-grpc-kit:proto-gen",
"options": {
"protoDirs": "shared/protos/src/raw",
"outDir": "shared/protos/src/lib/proto",
"indexPath": "shared/protos/src/index.ts",
"pluginOptions": "ngx-grpc.conf.js"
},
"inputs": ["{projectRoot}/src/raw/**/*.proto", "{workspaceRoot}/ngx-grpc.conf.js"],
"outputs": ["{projectRoot}/src/lib/proto"]
}
}
}ts-proto (framework-agnostic):
{
"targets": {
"proto-gen": {
"executor": "nx-grpc-kit:proto-gen",
"options": {
"protoDirs": ["protos/shared", "protos/services"],
"outDir": "src/generated",
"plugin": "ts-proto",
"pluginOptions": "esModuleInterop=true,outputServices=grpc-js"
}
}
}
}Executor options
| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| protoDirs | yes | — | Directory or array of directories with .proto files |
| outDir | yes | — | Output directory for generated TypeScript |
| plugin | no | protoc-gen-ng | Plugin preset name or path to custom binary |
| pluginOptions | no | — | Options passed to the plugin (format varies per plugin) |
| indexPath | no | — | Root barrel index.ts path (skip if empty) |
| excludePatterns | no | [] | Patterns to exclude from proto discovery |
| generateIndex | no | true | Auto-generate barrel index.ts per package |
| fixDashDirectories | no | true | Rename dash dirs to underscores + fix imports |
| protocPath | no | auto-detected | Custom protoc binary path |
| extraArgs | no | [] | Extra flags passed to protoc |
| submoduleUpdate | no | false | Sync git submodule before codegen (skips codegen if SHA unchanged) |
| watch | no | false | Watch protoDirs for .proto changes and auto-rebuild (debounced 500ms) |
Watch mode
nx run protos:proto-gen --watchWatches protoDirs for .proto file changes and re-runs codegen automatically with 500ms debounce.
Local development
When testing via npm link or npm install /local/path, Angular/Vite aggressively caches pre-bundled deps in .angular/cache/ and node_modules/.vite/. Rebuilding the lib source won't invalidate these caches. To pick up changes:
rm -rf .angular/cache node_modules/.vite
# restart dev serverRoadmap
withTracing(config)— OpenTelemetry-compatible trace ID injection (traceparentheader)withRateLimit(config)— Client-side token bucket rate limiting per methodnx g nx-grpc-kit:store— Scaffold NgRx Signal Store pair (data + action) from proto servicenx g nx-grpc-kit:init— Workspace setup generator- Package split —
nx-grpc-kit(Node/executor) +@nx-grpc-kit/interceptors(browser)
License
MIT
