c8y-nitro
v0.4.0
Published
Lightning fast Cumulocity IoT microservice development powered by Nitro
Downloads
235
Maintainers
Readme
c8y-nitro
Lightning fast Cumulocity IoT microservice development powered by Nitro.
Features
- ⚡️ Lightning Fast - Built on Nitro's high performance engine
- 🔧 Fully Configurable - Everything configured via module options
- 📁 Auto Zip Creation - Automatically generates the deployable microservice zip
- 🎯 API Client Generation - Creates Cumulocity-compatible Angular API clients
- 📦 Built-in Probes - Automatic setup for liveliness and readiness probes
- 🚀 Hot Module Reload - Instant feedback during development
- 🔥 File-based Routing - Auto-discovered routes from your file structure
- 🛠️ TypeScript First - Full type safety with excellent DX
- 🔄 Auto-Bootstrap - Automatically registers and configures your microservice in development
Quick Start
The fastest way to get started is using the c8y-nitro-starter template:
pnpm dlx giget@latest gh:schplitt/c8y-nitro-starter my-microservice
cd my-microservice
pnpm installConfigure your development tenant in .env:
C8Y_BASEURL=https://your-tenant.cumulocity.com
C8Y_DEVELOPMENT_TENANT=t12345
C8Y_DEVELOPMENT_USER=your-username
C8Y_DEVELOPMENT_PASSWORD=your-passwordThen start developing:
pnpm devInstallation
pnpm add c8y-nitro nitro@latestUsage
Configure your Cumulocity microservice in nitro.config.ts:
import c8y from 'c8y-nitro'
export default defineNitroConfig({
preset: 'node-server', // or "node-cluster", Required!
c8y: {
// c8y-nitro configuration options go here
},
modules: [c8y()],
})Prerequisites
c8y-nitro requires:
preset- must be a node preset (node-serverornode-cluster)
Recommended:
builder: 'rolldown'- for faster build times
Optional:
experimental.asyncContext: true- enables using utility functions without passing event/request parameters
Getting Started
Create a .env or .env.local file with your development tenant credentials:
C8Y_BASEURL=https://your-tenant.cumulocity.com
C8Y_DEVELOPMENT_TENANT=t12345
C8Y_DEVELOPMENT_USER=your-username
C8Y_DEVELOPMENT_PASSWORD=your-passwordThen simply run pnpm dev - that's it! The module will automatically:
- Check if the microservice exists on the tenant
- Create it if needed (or use existing one without overwriting)
- Subscribe your tenant to the microservice
- Retrieve and save bootstrap credentials to your env file
After auto-bootstrap, your env file will contain:
C8Y_BOOTSTRAP_TENANT=<bootstrap-tenant-id>
C8Y_BOOTSTRAP_USER=<bootstrap-username>
C8Y_BOOTSTRAP_PASSWORD=<generated-password>Manual Bootstrap: For more control or troubleshooting, you can use the CLI bootstrap command to manually register your microservice.
Disable Auto-Bootstrap: Set
skipBootstrap: truein your c8y config to disable auto-bootstrap entirely. This is useful in CI/CD pipelines or when you want to manage bootstrap manually.
Automatic Zip Creation
c8y-nitro automatically generates a ready-to-deploy microservice zip package after each build. The process includes:
- Dockerfile Generation - Creates an optimized Dockerfile using Node.js 22-slim
- Docker Image Build - Builds and saves the Docker image to
image.tar - Manifest Generation - Creates
cumulocity.jsonfrom your package.json and configuration - Zip Package - Combines
image.tarandcumulocity.jsoninto a deployable zip file
Note: Docker must be installed and available in your PATH.
The generated zip file (default: <package-name>-<version>.zip in root directory) is ready to upload directly to Cumulocity.
Manifest Configuration
The cumulocity.json manifest is automatically generated from your package.json and can be customized via the manifest option.
Auto-generated from package.json:
name(scope stripped),version- from package fieldsprovider.name- fromauthorfieldprovider.domain- fromauthor.urlorhomepageprovider.support- frombugsorauthor.emailcontextPath- defaults to package name
For all available manifest options, see the Cumulocity Microservice Manifest documentation.
Note: Custom roles defined in the manifest are automatically available as TypeScript types for use in middleware and runtime code during development.
Note: Health probe endpoints (
/_c8y_nitro/livenessand/_c8y_nitro/readiness) are automatically injected if not manually defined.
Cache Configuration
Credential caching can be configured to optimize performance. By default, subscribed tenant credentials are cached for 10 minutes.
export default defineNitroConfig({
c8y: {
cache: {
credentialsTTL: 300, // Cache credentials for 5 minutes (in seconds)
defaultTenantOptionsTTL: 600, // Default cache for tenant options (in seconds)
tenantOptions: {
'myOption': 300, // Per-key override: 5 minutes
'credentials.secret': 60, // Per-key override: 1 minute
},
}
},
modules: [c8y()],
})You can also override these at runtime using environment variables:
NITRO_C8Y_CREDENTIALS_CACHE_TTL=300
NITRO_C8Y_DEFAULT_TENANT_OPTIONS_TTL=300Note: The credentials cache is used by
useSubscribedTenantCredentials()anduseDeployedTenantCredentials()utilities. Both share the same cache.
Development User Injection
During development, c8y-nitro automatically injects your development user credentials into all requests. This allows you to test authentication and authorization middlewares locally.
The module uses the development credentials from your .env file:
C8Y_DEVELOPMENT_TENANT=t12345
C8Y_DEVELOPMENT_USER=your-username
C8Y_DEVELOPMENT_PASSWORD=your-passwordThis enables testing of access control middlewares like hasUserRequiredRole() and isUserFromAllowedTenant() without needing to manually set authorization headers.
Managing Development User Roles
Use the CLI roles command to assign or remove your microservice's custom roles to your development user:
pnpm dlx c8y-nitro rolesThis interactive command lets you select which roles from your manifest to assign to your development user for testing.
API Client Generation
For monorepo architectures, c8y-nitro can generate TypeScript Angular services that provide fully typed access to your microservice routes.
Configuration
export default defineNitroConfig({
c8y: {
apiClient: {
dir: '../ui/src/app/services', // Output directory for generated client
contextPath: 'my-service' // Optional: override context path
}
},
modules: [c8y()],
})Generated Client
The generated service creates one method per route with automatic type inference:
// Generated: my-serviceAPIClient.ts
@Injectable({ providedIn: 'root' })
export class GeneratedMyServiceAPIClient {
async GETHealth(): Promise<{ status: string }> { }
async GETUsersById(params: { id: string | number }): Promise<User> { }
async POSTUsers(body: CreateUserDto): Promise<User> { }
}Usage in Angular
import { GeneratedMyServiceAPIClient } from './services/my-serviceAPIClient'
@Component({
/**
* ...
*/
})
export class MyComponent {
private api = inject(GeneratedMyServiceAPIClient)
async ngOnInit() {
const health = await this.api.GETHealth()
const user = await this.api.GETUsersById({ id: 123 })
}
}Note: The client regenerates automatically when routes change during development.
Logging
c8y-nitro builds on evlog to provide structured wide-event logging, one comprehensive log per request that accumulates all relevant context rather than scattering individual log lines throughout your code.
evlog is automatically configured, no extra setup required. The service name is derived from your package name.
useLogger
Use useLogger(event) in your route handlers to get a request-scoped logger. The logger accumulates context throughout the request lifetime and emits a single wide event when the response is sent.
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'c8y-nitro/utils'
export default defineHandler(async (event) => {
const log = useLogger(event)
const user = await useUser(event)
log.set({ action: 'process-order', user: { id: user.userName } })
// Add more context as it becomes available
log.set({ order: { id: '42', total: 9999 } })
return { success: true }
})Note:
useLoggerrequires theeventparameter. If you enableexperimental.asyncContext: truein your Nitro config, you can access the logger anywhere in the call stack viauseRequest()fromnitro/context— see the evlog Nitro v3 setup for details.
createError
Use createError from c8y-nitro/utils instead of Nitro's built-in error helper to get richer, structured error responses. This adds why, fix, and link fields that are:
- Logged as part of the wide event so you can see exactly what went wrong without guessing
- Returned in the JSON response body so clients can display actionable context
import { defineHandler } from 'nitro/h3'
import { useLogger, createError } from 'c8y-nitro/utils'
export default defineHandler(async (event) => {
const log = useLogger(event)
log.set({ action: 'payment', userId: 'user_123' })
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer (insufficient funds)',
fix: 'Try a different payment method or contact your bank',
link: 'https://docs.example.com/payments/declined',
})
})The error response returned to the client:
{
"message": "Payment failed",
"status": 402,
"data": {
"why": "Card declined by issuer (insufficient funds)",
"fix": "Try a different payment method or contact your bank",
"link": "https://docs.example.com/payments/declined"
}
}Tip: Always prefer
createErrorfromc8y-nitro/utils. It ensures the error is captured in the wide log event with full context, making investigation straightforward.
createLogger (standalone)
For code that runs outside a request handler — background jobs, queue workers, event-driven workflows, scheduled tasks — use createLogger() to get the same wide-event logger interface without needing an HTTP event.
import { createLogger } from 'c8y-nitro/utils'
export async function processSubscriptionRenewal(tenantId: string) {
const log = createLogger({ job: 'subscription-renewal', tenantId })
log.set({ subscription: { id: 'sub_123', plan: 'pro' } })
// ... do work ...
log.set({ result: 'renewed' })
log.emit() // Must call emit() manually outside request context
}This is useful for Cumulocity notification workflows where your microservice reacts to platform events (device management, alarms, etc.) outside of the standard request/response cycle.
Note: Unlike
useLogger,createLoggerdoes not auto-emit at request end. You must calllog.emit()manually when the work is complete.
For more on wide events, structured errors, and advanced configuration (sampling, draining to Axiom/Loki, enrichers), see the evlog documentation.
Logging Utilities
| Function | Description | Request Context |
| ---------------------- | ------------------------------------------------------------- | :-------------: |
| useLogger(event) | Get the request-scoped wide-event logger | ✅ |
| createLogger(ctx?) | Create a standalone wide-event logger; call emit() manually | ❌ |
| createError(options) | Create a structured error with why, fix, link | ❌ |
Utilities
c8y-nitro provides several utility functions to simplify common tasks in Cumulocity microservices.
To use these utilities, simply import them from c8y-nitro/utils:
import { useUser, useUserClient } from 'c8y-nitro/utils'Usage
All utility functions that require request context accept either an H3Event or ServerRequest parameter:
// Pass the event/request parameter
export default defineHandler(async (event) => {
const user = await useUser(event)
const client = useUserClient(event)
return { user }
})Optional: Using with asyncContext
If you enable experimental.asyncContext: true in your Nitro config, you can use Nitro's useRequest() to avoid passing the event through deeply nested function calls:
import { useRequest } from 'nitro/context'
export default defineHandler(async (event) => {
// Deep nested function - no need to pass event down
return await someDeepFunction()
})
async function someDeepFunction() {
return await anotherFunction()
}
async function anotherFunction() {
// Use useRequest() to get the request in any nested function
const request = useRequest()
const user = await useUser(request)
return { user }
}Credentials
| Function | Description | Request Context |
| ---------------------------------- | ------------------------------------------------------------------ | :-------------: |
| useSubscribedTenantCredentials() | Get credentials for all subscribed tenants (cached, default 10min) | ❌ |
| useDeployedTenantCredentials() | Get credentials for the deployed tenant (cached, default 10min) | ❌ |
| useUserTenantCredentials() | Get credentials for the current user's tenant | ✅ |
Note:
useDeployedTenantCredentials()shares its cache withuseSubscribedTenantCredentials(). Both functions support.invalidate()and.refresh()methods. Invalidating or refreshing one will affect the other.Cache Duration: The cache TTL is configurable via the
cache.credentialsTTLoption orNITRO_C8Y_CREDENTIALS_CACHE_TTLenvironment variable. See Cache Configuration for details.
Tenant Options
| Function | Description | Request Context |
| ------------------- | -------------------------------------------------------- | :-------------: |
| useTenantOption() | Get a tenant option value by key (cached, default 10min) | ❌ |
Fetch tenant options (settings) configured for your microservice:
import { useTenantOption } from 'c8y-nitro/utils'
export default defineHandler(async (event) => {
// Fetch a tenant option
const value = await useTenantOption('myOption')
// Fetch an encrypted secret
const secret = await useTenantOption('credentials.apiKey')
// Cache management
await useTenantOption.invalidate('myOption') // Invalidate specific key
const fresh = await useTenantOption.refresh('myOption') // Force refresh
await useTenantOption.invalidateAll() // Invalidate all accessed keys
await useTenantOption.refreshAll() // Refresh all accessed keys
return { value, secret, fresh }
})Define your settings in the manifest to get type-safe keys:
export default defineNitroConfig({
c8y: {
manifest: {
settings: [
{ key: 'myOption', defaultValue: 'default' },
{ key: 'credentials.secret' }, // Encrypted option
],
settingsCategory: 'my-service', // Optional, defaults to contextPath/name
requiredRoles: ['ROLE_OPTION_MANAGEMENT_READ'], // Required for reading tenant options
},
},
modules: [c8y()],
})Important: To read tenant options, your microservice must have the
ROLE_OPTION_MANAGEMENT_READrole inmanifest.requiredRoles. Without this role, API calls will fail with a 403 Forbidden error.
Note on Encrypted Options: Keys prefixed with
credentials.are stored encrypted by Cumulocity. The value is automatically decrypted when fetched if your microservice is the owner of the option (the option's category matches your microservice'ssettingsCategory,contextPath, or name). Thecredentials.prefix is automatically stripped when calling the API.
Note on Missing Options: If a tenant option is not set (404 Not Found),
useTenantOption()returnsundefinedinstead of throwing an error. Other errors (e.g., 403 Forbidden) are thrown normally.
Resources
| Function | Description | Request Context |
| ---------------- | ---------------------------------- | :-------------: |
| useUser() | Fetch current user from Cumulocity | ✅ |
| useUserRoles() | Get roles of the current user | ✅ |
Client
| Function | Description | Request Context |
| ------------------------------ | --------------------------------------------------- | :-------------: |
| useUserClient() | Create client authenticated with user's credentials | ✅ |
| useUserTenantClient() | Create client for user's tenant (microservice user) | ✅ |
| useSubscribedTenantClients() | Create clients for all subscribed tenants | ❌ |
| useDeployedTenantClient() | Create client for the deployed tenant | ❌ |
Middleware
| Function | Description | Request Context |
| ------------------------------------------ | ----------------------------------------- | :-------------: |
| hasUserRequiredRole(role\|roles) | Check if user has required role(s) | ✅ |
| isUserFromAllowedTenant(tenant\|tenants) | Check if user is from allowed tenant(s) | ✅ |
| isUserFromDeployedTenant() | Check if user is from the deployed tenant | ✅ |
CLI Commands
| Command | Description |
| ----------- | ------------------------------------------------------- |
| bootstrap | Manually register microservice and retrieve credentials |
| roles | Manage development user roles |
| options | Manage tenant options on development tenant |
For more information, run:
pnpm dlx c8y-nitro -hDevelopment
# Install dependencies
pnpm install
# Run dev watcher
pnpm dev
# Build for production
pnpm build
# Run tests (watch mode)
pnpm test
# Run tests once
pnpm test:runTesting
Tests are organized in two categories:
- Unit tests (
tests/unit/) — Test individual functions in isolation - Server tests (
tests/server/) — Integration tests that spin up a Nitro dev server with the c8y-nitro module
Server tests use Nitro's virtual modules to mock @c8y/client at build time, allowing full integration testing without real Cumulocity API calls. See AGENTS.md for implementation details.
License
MIT
