@venizia/ignis-docs 0.0.7 → 0.0.8-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-server/common/paths.d.ts +4 -2
- package/dist/mcp-server/common/paths.d.ts.map +1 -1
- package/dist/mcp-server/common/paths.js +8 -6
- package/dist/mcp-server/common/paths.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.js +7 -7
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +3 -3
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +1 -1
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js +1 -1
- package/package.json +1 -1
- package/wiki/best-practices/api-usage-examples.md +9 -9
- package/wiki/best-practices/architectural-patterns.md +19 -3
- package/wiki/best-practices/architecture-decisions.md +6 -6
- package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/code-style-standards/function-patterns.md +2 -2
- package/wiki/best-practices/code-style-standards/index.md +2 -2
- package/wiki/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/wiki/best-practices/code-style-standards/route-definitions.md +4 -4
- package/wiki/best-practices/data-modeling.md +1 -1
- package/wiki/best-practices/deployment-strategies.md +1 -1
- package/wiki/best-practices/error-handling.md +2 -2
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/{references → extensions}/components/authentication/api.md +12 -20
- package/wiki/{references → extensions}/components/authentication/errors.md +1 -1
- package/wiki/{references → extensions}/components/authentication/index.md +5 -8
- package/wiki/{references → extensions}/components/authentication/usage.md +20 -36
- package/wiki/{references → extensions}/components/authorization/api.md +62 -13
- package/wiki/{references → extensions}/components/authorization/errors.md +12 -7
- package/wiki/{references → extensions}/components/authorization/index.md +93 -6
- package/wiki/{references → extensions}/components/authorization/usage.md +42 -4
- package/wiki/{references → extensions}/components/health-check.md +5 -4
- package/wiki/{references → extensions}/components/index.md +2 -0
- package/wiki/{references → extensions}/components/mail/index.md +1 -1
- package/wiki/{references → extensions}/components/request-tracker.md +1 -1
- package/wiki/{references → extensions}/components/socket-io/api.md +2 -2
- package/wiki/{references → extensions}/components/socket-io/errors.md +2 -0
- package/wiki/{references → extensions}/components/socket-io/index.md +24 -20
- package/wiki/{references → extensions}/components/socket-io/usage.md +2 -2
- package/wiki/{references → extensions}/components/static-asset/api.md +14 -15
- package/wiki/{references → extensions}/components/static-asset/errors.md +3 -1
- package/wiki/{references → extensions}/components/static-asset/index.md +158 -89
- package/wiki/{references → extensions}/components/static-asset/usage.md +8 -5
- package/wiki/{references → extensions}/components/swagger.md +3 -3
- package/wiki/{references → extensions}/components/template/index.md +4 -4
- package/wiki/{references → extensions}/components/template/setup-page.md +1 -1
- package/wiki/{references → extensions}/components/template/single-page.md +1 -1
- package/wiki/{references → extensions}/components/websocket/api.md +7 -6
- package/wiki/{references → extensions}/components/websocket/errors.md +17 -3
- package/wiki/{references → extensions}/components/websocket/index.md +17 -11
- package/wiki/{references → extensions}/components/websocket/usage.md +2 -2
- package/wiki/{references → extensions}/helpers/crypto/index.md +1 -1
- package/wiki/{references → extensions}/helpers/env/index.md +9 -5
- package/wiki/{references → extensions}/helpers/error/index.md +2 -7
- package/wiki/{references → extensions}/helpers/index.md +18 -6
- package/wiki/{references → extensions}/helpers/kafka/admin.md +13 -1
- package/wiki/{references → extensions}/helpers/kafka/consumer.md +28 -28
- package/wiki/{references → extensions}/helpers/kafka/examples.md +19 -19
- package/wiki/{references → extensions}/helpers/kafka/index.md +51 -48
- package/wiki/{references → extensions}/helpers/kafka/producer.md +18 -18
- package/wiki/{references → extensions}/helpers/kafka/schema-registry.md +25 -25
- package/wiki/{references → extensions}/helpers/logger/index.md +2 -2
- package/wiki/{references → extensions}/helpers/queue/index.md +400 -4
- package/wiki/{references → extensions}/helpers/storage/api.md +170 -10
- package/wiki/{references → extensions}/helpers/storage/index.md +44 -8
- package/wiki/{references → extensions}/helpers/template/index.md +1 -1
- package/wiki/{references → extensions}/helpers/testing/index.md +4 -4
- package/wiki/{references → extensions}/helpers/types/index.md +63 -16
- package/wiki/{references → extensions}/helpers/websocket/index.md +1 -1
- package/wiki/extensions/index.md +48 -0
- package/wiki/guides/core-concepts/application/bootstrapping.md +55 -37
- package/wiki/guides/core-concepts/application/index.md +95 -35
- package/wiki/guides/core-concepts/components-guide.md +23 -19
- package/wiki/guides/core-concepts/components.md +34 -10
- package/wiki/guides/core-concepts/dependency-injection.md +99 -34
- package/wiki/guides/core-concepts/grpc-controllers.md +295 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +27 -8
- package/wiki/guides/core-concepts/persistent/models.md +43 -1
- package/wiki/guides/core-concepts/persistent/repositories.md +75 -8
- package/wiki/guides/core-concepts/persistent/transactions.md +38 -8
- package/wiki/guides/core-concepts/{controllers.md → rest-controllers.md} +30 -33
- package/wiki/guides/core-concepts/services.md +19 -5
- package/wiki/guides/get-started/5-minute-quickstart.md +6 -7
- package/wiki/guides/get-started/philosophy.md +1 -1
- package/wiki/guides/index.md +2 -2
- package/wiki/guides/reference/glossary.md +7 -7
- package/wiki/guides/reference/mcp-docs-server.md +1 -1
- package/wiki/guides/tutorials/building-a-crud-api.md +2 -2
- package/wiki/guides/tutorials/complete-installation.md +17 -14
- package/wiki/guides/tutorials/ecommerce-api.md +18 -18
- package/wiki/guides/tutorials/realtime-chat.md +8 -8
- package/wiki/guides/tutorials/testing.md +2 -2
- package/wiki/index.md +4 -3
- package/wiki/references/base/application.md +341 -21
- package/wiki/references/base/bootstrapping.md +43 -13
- package/wiki/references/base/components.md +259 -8
- package/wiki/references/base/controllers.md +556 -253
- package/wiki/references/base/datasources.md +159 -79
- package/wiki/references/base/dependency-injection.md +299 -48
- package/wiki/references/base/filter-system/application-usage.md +18 -2
- package/wiki/references/base/filter-system/array-operators.md +14 -6
- package/wiki/references/base/filter-system/comparison-operators.md +9 -3
- package/wiki/references/base/filter-system/default-filter.md +28 -3
- package/wiki/references/base/filter-system/fields-order-pagination.md +17 -13
- package/wiki/references/base/filter-system/index.md +169 -11
- package/wiki/references/base/filter-system/json-filtering.md +51 -18
- package/wiki/references/base/filter-system/list-operators.md +4 -3
- package/wiki/references/base/filter-system/logical-operators.md +7 -2
- package/wiki/references/base/filter-system/null-operators.md +50 -0
- package/wiki/references/base/filter-system/quick-reference.md +82 -243
- package/wiki/references/base/filter-system/range-operators.md +7 -1
- package/wiki/references/base/filter-system/tips.md +34 -7
- package/wiki/references/base/filter-system/use-cases.md +6 -5
- package/wiki/references/base/grpc-controllers.md +984 -0
- package/wiki/references/base/index.md +32 -24
- package/wiki/references/base/middleware.md +347 -0
- package/wiki/references/base/models.md +390 -46
- package/wiki/references/base/providers.md +14 -14
- package/wiki/references/base/repositories/advanced.md +84 -69
- package/wiki/references/base/repositories/index.md +447 -12
- package/wiki/references/base/repositories/mixins.md +103 -98
- package/wiki/references/base/repositories/relations.md +129 -45
- package/wiki/references/base/repositories/soft-deletable.md +104 -23
- package/wiki/references/base/services.md +94 -14
- package/wiki/references/index.md +12 -10
- package/wiki/references/quick-reference.md +98 -65
- package/wiki/references/utilities/crypto.md +21 -4
- package/wiki/references/utilities/date.md +25 -7
- package/wiki/references/utilities/index.md +26 -24
- package/wiki/references/utilities/jsx.md +54 -54
- package/wiki/references/utilities/module.md +8 -6
- package/wiki/references/utilities/parse.md +16 -9
- package/wiki/references/utilities/performance.md +22 -7
- package/wiki/references/utilities/promise.md +19 -16
- package/wiki/references/utilities/request.md +48 -26
- package/wiki/references/utilities/schema.md +69 -6
- package/wiki/references/utilities/statuses.md +131 -140
- /package/wiki/{references → extensions}/components/mail/api.md +0 -0
- /package/wiki/{references → extensions}/components/mail/errors.md +0 -0
- /package/wiki/{references → extensions}/components/mail/usage.md +0 -0
- /package/wiki/{references → extensions}/components/template/api-page.md +0 -0
- /package/wiki/{references → extensions}/components/template/errors-page.md +0 -0
- /package/wiki/{references → extensions}/components/template/usage-page.md +0 -0
- /package/wiki/{references → extensions}/helpers/cron/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/inversion/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/network/api.md +0 -0
- /package/wiki/{references → extensions}/helpers/network/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/redis/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/socket-io/api.md +0 -0
- /package/wiki/{references → extensions}/helpers/socket-io/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/template/single-page.md +0 -0
- /package/wiki/{references → extensions}/helpers/uid/index.md +0 -0
- /package/wiki/{references → extensions}/helpers/websocket/api.md +0 -0
- /package/wiki/{references → extensions}/helpers/worker-thread/index.md +0 -0
- /package/wiki/{references → extensions}/src-details/mcp-server.md +0 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: gRPC Controllers Reference
|
|
3
|
+
description: Technical reference for gRPC controller classes, RPC decorators, ConnectRPC adapter, and component integration
|
|
4
|
+
difficulty: intermediate
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Deep Dive: gRPC Controllers
|
|
8
|
+
|
|
9
|
+
Technical reference for gRPC controller classes -- the foundation for building gRPC services in Ignis, powered by [ConnectRPC](https://connectrpc.com/).
|
|
10
|
+
|
|
11
|
+
Ignis gRPC controllers follow the same patterns as REST controllers (decorator-based routing, `binding()` method, DI integration) while bridging to ConnectRPC's universal handler system. REST and gRPC controllers coexist in the same application, sharing the same DI container, middleware pipeline, and lifecycle.
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- `packages/core/src/base/controllers/grpc/abstract.ts`
|
|
15
|
+
- `packages/core/src/base/controllers/grpc/base.ts`
|
|
16
|
+
- `packages/core/src/base/controllers/grpc/adapter.ts`
|
|
17
|
+
- `packages/core/src/base/controllers/grpc/common/types.ts`
|
|
18
|
+
- `packages/core/src/base/metadata/routes/rpc.ts`
|
|
19
|
+
- `packages/core/src/components/controller/grpc/grpc.component.ts`
|
|
20
|
+
- `packages/core/src/components/controller/grpc/common/types.ts`
|
|
21
|
+
|
|
22
|
+
## Quick Reference
|
|
23
|
+
|
|
24
|
+
| Item | Description |
|
|
25
|
+
|------|-------------|
|
|
26
|
+
| **AbstractGrpcController** | Abstract base class with RPC registration, ConnectRPC adapter mounting, idempotent `configure()` |
|
|
27
|
+
| **BaseGrpcController** | Recommended concrete base class with `bindRoute()` and `defineRoute()` implementations |
|
|
28
|
+
| **GrpcRequestAdapter** | Internal bridge from Ignis handlers to ConnectRPC universal handlers via `AsyncLocalStorage` |
|
|
29
|
+
| **GrpcComponent** | Auto-discovers gRPC controllers and mounts them on the application router |
|
|
30
|
+
| **@controller** | Class decorator with `transport: ControllerTransports.GRPC` and `service` field |
|
|
31
|
+
| **@unary** | Method decorator for unary RPCs |
|
|
32
|
+
| **@serverStream** | Method decorator for server-streaming RPCs (**unsupported -- throws at boot**) |
|
|
33
|
+
| **@clientStream** | Method decorator for client-streaming RPCs (**unsupported -- throws at boot**) |
|
|
34
|
+
| **@bidiStream** | Method decorator for bidirectional-streaming RPCs (**unsupported -- throws at boot**) |
|
|
35
|
+
| **@rpc** | Generic method decorator (requires explicit `method` in configs) |
|
|
36
|
+
|
|
37
|
+
> [!WARNING]
|
|
38
|
+
> **Current version supports unary RPCs only.** The `@serverStream`, `@clientStream`, and `@bidiStream` decorators still exist and set metadata correctly, but `BaseGrpcController.registerRoute()` will throw a clear error at boot time if a non-unary RPC is registered. This is because the Connect protocol over HTTP/1.1 cannot support streaming. The decorators are preserved for forward compatibility.
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
gRPC support requires the following peer dependencies:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun add @connectrpc/connect @bufbuild/protobuf
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Package | Purpose |
|
|
49
|
+
|---------|---------|
|
|
50
|
+
| `@connectrpc/connect` | ConnectRPC router, universal handlers, protocol bridge |
|
|
51
|
+
| `@bufbuild/protobuf` | Protobuf code generation, `create()` for constructing response messages |
|
|
52
|
+
|
|
53
|
+
For client-side usage (e.g., test clients), you also need a transport package:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bun add @connectrpc/connect-web
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> [!NOTE]
|
|
60
|
+
> These are **optional** peer dependencies. They are only loaded at runtime when a gRPC controller is configured, via `createRequire` from the application's `node_modules`. If the deps are missing, `GrpcRequestAdapter.build()` throws a clear error at startup via `validateModule()`.
|
|
61
|
+
|
|
62
|
+
### Protobuf Code Generation
|
|
63
|
+
|
|
64
|
+
Use `buf` or `protoc-gen-es` to generate TypeScript code from `.proto` files:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# buf.gen.yaml
|
|
68
|
+
version: v2
|
|
69
|
+
plugins:
|
|
70
|
+
- local: protoc-gen-es
|
|
71
|
+
out: generated
|
|
72
|
+
opt: target=ts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
buf generate proto/greeter.proto
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The generated output includes:
|
|
80
|
+
- **Service descriptors** (e.g., `GreeterService`) -- passed to `@controller({ service })`
|
|
81
|
+
- **Message schemas** (e.g., `SayHelloResponseSchema`) -- used with `create()` to build responses
|
|
82
|
+
- **TypeScript types** (e.g., `SayHelloRequest`, `SayHelloResponse`) -- for handler signatures
|
|
83
|
+
|
|
84
|
+
## `BaseGrpcController`
|
|
85
|
+
|
|
86
|
+
The recommended base class for gRPC controllers. Extends `AbstractGrpcController` with concrete `bindRoute()` and `defineRoute()` implementations.
|
|
87
|
+
|
|
88
|
+
### Constructor Options
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
interface IGrpcControllerOptions {
|
|
92
|
+
scope: string;
|
|
93
|
+
path?: string; // Falls back to @controller decorator path if not provided
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `scope` is used for scoped logging (`this.logger.for('methodName')`). The `path` defines the HTTP mount point for the ConnectRPC handlers; when both the constructor and `@controller` decorator specify a path, the decorator takes precedence.
|
|
98
|
+
|
|
99
|
+
### Generic Parameters
|
|
100
|
+
|
|
101
|
+
`BaseGrpcController` accepts five generic parameters:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
class BaseGrpcController<
|
|
105
|
+
RouteEnv extends Env = Env,
|
|
106
|
+
RouteSchema extends Schema = {},
|
|
107
|
+
BasePath extends string = '/',
|
|
108
|
+
ServiceType = unknown,
|
|
109
|
+
ConfigurableOptions extends object = {},
|
|
110
|
+
>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Parameter | Default | Description |
|
|
114
|
+
|-----------|---------|-------------|
|
|
115
|
+
| `RouteEnv` | `Env` | Hono environment type for typed context access |
|
|
116
|
+
| `RouteSchema` | `{}` | Hono schema type |
|
|
117
|
+
| `BasePath` | `'/'` | Base path string literal type |
|
|
118
|
+
| `ServiceType` | `unknown` | ConnectRPC service descriptor type |
|
|
119
|
+
| `ConfigurableOptions` | `{}` | Extra options passed to `configure()` |
|
|
120
|
+
|
|
121
|
+
`AbstractGrpcController` differs by defaulting `ServiceType` to `Parameters<ConnectRouter['service']>[0]` (the actual ConnectRPC service descriptor type), providing stricter type checking on the `service` field.
|
|
122
|
+
|
|
123
|
+
### The `@controller` Decorator
|
|
124
|
+
|
|
125
|
+
gRPC controllers use the same `@controller` decorator as REST controllers, with two additional fields:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
@controller({
|
|
129
|
+
path: '/grpc',
|
|
130
|
+
transport: ControllerTransports.GRPC,
|
|
131
|
+
service: GreeterServiceDef, // Generated ConnectRPC service descriptor
|
|
132
|
+
})
|
|
133
|
+
export class GreeterController extends BaseGrpcController {
|
|
134
|
+
// ...
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Field | Type | Required | Description |
|
|
139
|
+
|-------|------|----------|-------------|
|
|
140
|
+
| `path` | `string` | Yes | HTTP base path for this controller's RPC endpoints |
|
|
141
|
+
| `transport` | `ControllerTransports.GRPC` | Yes | Marks this controller for gRPC transport (picked up by `GrpcComponent`) |
|
|
142
|
+
| `service` | `ServiceType` | Yes | ConnectRPC service descriptor from generated protobuf code |
|
|
143
|
+
| `tags` | `string[]` | No | Metadata tags (inherited from base controller metadata) |
|
|
144
|
+
| `description` | `string` | No | Controller description (inherited from base controller metadata) |
|
|
145
|
+
|
|
146
|
+
> [!NOTE]
|
|
147
|
+
> If `service` is missing or falsy at configure time, the `GrpcComponent` logs a warning and skips the controller entirely -- no routes are mounted.
|
|
148
|
+
|
|
149
|
+
### Route Definition Patterns
|
|
150
|
+
|
|
151
|
+
Like REST controllers, gRPC controllers support three route definition patterns:
|
|
152
|
+
|
|
153
|
+
#### 1. Decorator-Based (Recommended)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
@controller({
|
|
157
|
+
path: '/grpc',
|
|
158
|
+
transport: ControllerTransports.GRPC,
|
|
159
|
+
service: GreeterServiceDef,
|
|
160
|
+
})
|
|
161
|
+
export class GreeterController extends BaseGrpcController {
|
|
162
|
+
override binding() {}
|
|
163
|
+
|
|
164
|
+
@unary({ configs: { name: 'sayHello' } })
|
|
165
|
+
async sayHello(opts: { request: SayHelloRequest }): Promise<SayHelloResponse> {
|
|
166
|
+
return create(SayHelloResponseSchema, { message: `Hello, ${opts.request.name}!` });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Decorator-based RPCs are auto-discovered during `configure()` via `registerRpcsFromRegistry()`. The `binding()` method can be left empty if all routes use decorators.
|
|
172
|
+
|
|
173
|
+
#### 2. `defineRoute()` -- Imperative
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
override binding() {
|
|
177
|
+
this.defineRoute({
|
|
178
|
+
configs: { name: 'sayHello', method: GRPC.Methods.UNARY },
|
|
179
|
+
handler: async (opts) => {
|
|
180
|
+
return create(SayHelloResponseSchema, { message: `Hello!` });
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 3. `bindRoute().to()` -- Fluent
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
override binding() {
|
|
190
|
+
this.bindRoute({
|
|
191
|
+
configs: { name: 'sayHello', method: GRPC.Methods.UNARY },
|
|
192
|
+
}).to({
|
|
193
|
+
handler: async (opts) => {
|
|
194
|
+
return create(SayHelloResponseSchema, { message: `Hello!` });
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### The `binding()` Method
|
|
201
|
+
|
|
202
|
+
An abstract method you override to register RPCs using `defineRoute()` or `bindRoute()`. Called during `configure()` before decorator-based RPCs are registered. If you only use decorators, provide an empty implementation:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
override binding() {}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### The `definitions` Property
|
|
209
|
+
|
|
210
|
+
A `Record<string, IRpcRegistration>` that stores all registered RPC handlers keyed by their proto method name. Populated by both decorator-based and imperative registration. The `GrpcRequestAdapter` reads this to build ConnectRPC handlers.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
interface IRpcRegistration<RouteEnv extends Env = Env> {
|
|
214
|
+
configs: IRpcMetadata;
|
|
215
|
+
handler: TRpcHandler<unknown, unknown, RouteEnv>;
|
|
216
|
+
middlewares: TRpcMiddleware<RouteEnv>[]; // Pre-built auth middleware
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
If you register a handler with the same `name` as an existing one, it overwrites the previous handler with a warning.
|
|
221
|
+
|
|
222
|
+
### The `configure()` Lifecycle
|
|
223
|
+
|
|
224
|
+
The `configure()` method on `AbstractGrpcController` is idempotent (guarded by `isConfigured` flag). It runs the following steps in order:
|
|
225
|
+
|
|
226
|
+
1. **`binding()`** -- Your override, registers imperative/fluent routes
|
|
227
|
+
2. **`registerRpcsFromRegistry()`** -- Discovers decorator-based RPCs from `MetadataRegistry` and calls `bindRoute()` for each
|
|
228
|
+
3. **`GrpcRequestAdapter.build()`** -- Creates the ConnectRPC adapter and mounts it as Hono middleware on `this.router`
|
|
229
|
+
|
|
230
|
+
## RPC Decorators
|
|
231
|
+
|
|
232
|
+
All RPC decorators live in `packages/core/src/base/metadata/routes/rpc.ts`. They register metadata in the `MetadataRegistry`, which is read during `configure()`.
|
|
233
|
+
|
|
234
|
+
### `@rpc` -- Generic
|
|
235
|
+
|
|
236
|
+
The base decorator. Requires the full `IRpcMetadata` config including `method`:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
@rpc({ configs: { name: 'sayHello', method: GRPC.Methods.UNARY } })
|
|
240
|
+
async sayHello(opts: { request: SayHelloRequest }): Promise<SayHelloResponse> {
|
|
241
|
+
// ...
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `@unary`
|
|
246
|
+
|
|
247
|
+
Shorthand for `@rpc` with `method: 'unary'`. Single request, single response.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
@unary({ configs: { name: 'sayHello' } })
|
|
251
|
+
async sayHello(opts: { request: SayHelloRequest }): Promise<SayHelloResponse> {
|
|
252
|
+
return create(SayHelloResponseSchema, { message: `Hello, ${opts.request.name}!` });
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `@serverStream` (unsupported)
|
|
257
|
+
|
|
258
|
+
Shorthand for `@rpc` with `method: 'server_streaming'`. **Throws at boot time in the current version** -- streaming is not supported over HTTP/1.1 Connect protocol. Decorator preserved for forward compatibility.
|
|
259
|
+
|
|
260
|
+
### `@clientStream` (unsupported)
|
|
261
|
+
|
|
262
|
+
Shorthand for `@rpc` with `method: 'client_streaming'`. **Throws at boot time in the current version.**
|
|
263
|
+
|
|
264
|
+
### `@bidiStream` (unsupported)
|
|
265
|
+
|
|
266
|
+
Shorthand for `@rpc` with `method: 'bidi_streaming'`. **Throws at boot time in the current version.**
|
|
267
|
+
|
|
268
|
+
### Decorator Config
|
|
269
|
+
|
|
270
|
+
All decorators accept `{ configs: ... }` where configs extends `IRpcMetadata` (with `method` omitted for the shorthand variants):
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// @unary, @serverStream, @clientStream, @bidiStream
|
|
274
|
+
{ configs: Omit<IRpcMetadata, 'method'> }
|
|
275
|
+
|
|
276
|
+
// @rpc (generic)
|
|
277
|
+
{ configs: IRpcMetadata }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Type Definitions
|
|
281
|
+
|
|
282
|
+
### `IRpcMetadata`
|
|
283
|
+
|
|
284
|
+
Metadata stored per RPC method in the `MetadataRegistry`.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface IRpcMetadata {
|
|
288
|
+
/** Proto method name -- must match the RPC name in your .proto service definition. */
|
|
289
|
+
name: string;
|
|
290
|
+
/** RPC method type. */
|
|
291
|
+
method: TGrpcMethod; // 'unary' | 'server_streaming' | 'client_streaming' | 'bidi_streaming'
|
|
292
|
+
/** Per-RPC authentication config. */
|
|
293
|
+
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
294
|
+
/** Per-RPC authorization spec(s). */
|
|
295
|
+
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `IRpcRegistration`
|
|
300
|
+
|
|
301
|
+
Unified entry stored in the controller's `definitions` map. Combines metadata, handler function, and pre-built auth middleware.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
interface IRpcRegistration<RouteEnv extends Env = Env> {
|
|
305
|
+
configs: IRpcMetadata;
|
|
306
|
+
handler: TRpcHandler<unknown, unknown, RouteEnv>;
|
|
307
|
+
middlewares: TRpcMiddleware<RouteEnv>[];
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### `TRpcMiddleware`
|
|
312
|
+
|
|
313
|
+
Pre-built middleware function for gRPC auth enforcement, created by `AbstractGrpcController.buildRpcMiddlewares()`.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
type TRpcMiddleware<RouteEnv extends Env = Env> = (
|
|
317
|
+
context: TRouteContext<RouteEnv>,
|
|
318
|
+
next: Next,
|
|
319
|
+
) => ValueOrPromise<void | Response>;
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `TRpcHandler`
|
|
323
|
+
|
|
324
|
+
The handler signature for gRPC RPC methods. Receives the deserialized protobuf request and the Hono context (via `AsyncLocalStorage`).
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
type TRpcHandler<
|
|
328
|
+
RequestType = unknown,
|
|
329
|
+
ResponseType = unknown,
|
|
330
|
+
RouteEnv extends Env = Env,
|
|
331
|
+
> = (opts: {
|
|
332
|
+
request: RequestType;
|
|
333
|
+
context: TRouteContext<RouteEnv>;
|
|
334
|
+
}) => ValueOrPromise<ResponseType>;
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> [!NOTE]
|
|
338
|
+
> When using decorator-based RPCs, the handler method signature is `(opts: { request: RequestType }) => Promise<ResponseType>`. The `context` parameter is injected internally by the adapter and is not passed to the decorator-based handler method directly. The full `TRpcHandler` signature (with `context`) applies when using `defineRoute()` or `bindRoute()`.
|
|
339
|
+
|
|
340
|
+
### `IGrpcControllerOptions`
|
|
341
|
+
|
|
342
|
+
Constructor options for gRPC controllers.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface IGrpcControllerOptions {
|
|
346
|
+
scope: string;
|
|
347
|
+
path?: string;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### `IGrpcBindRouteOptions`
|
|
352
|
+
|
|
353
|
+
Fluent binding returned by `bindRoute()`.
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
interface IGrpcBindRouteOptions<RouteEnv extends Env = Env> {
|
|
357
|
+
configs: IRpcMetadata;
|
|
358
|
+
to: (opts: { handler: TRpcHandler<unknown, unknown, RouteEnv> }) => IGrpcDefineRouteOptions;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `IGrpcDefineRouteOptions`
|
|
363
|
+
|
|
364
|
+
Return type from both `defineRoute()` and `bindRoute().to()`.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
interface IGrpcDefineRouteOptions {
|
|
368
|
+
configs: IRpcMetadata;
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `IGrpcController`
|
|
373
|
+
|
|
374
|
+
The full interface that gRPC controllers implement. Extends `IConfigurable`.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
interface IGrpcController<
|
|
378
|
+
RouteEnv extends Env = Env,
|
|
379
|
+
RouteSchema extends Schema = {},
|
|
380
|
+
BasePath extends string = '/',
|
|
381
|
+
ServiceType = unknown,
|
|
382
|
+
ConfigurableOptions extends object = {},
|
|
383
|
+
> extends IConfigurable<ConfigurableOptions> {
|
|
384
|
+
service: ServiceType;
|
|
385
|
+
router: Hono<RouteEnv, RouteSchema, BasePath>;
|
|
386
|
+
definitions: Record<string, IRpcRegistration<RouteEnv>>;
|
|
387
|
+
|
|
388
|
+
getRouter(): Hono<RouteEnv, RouteSchema, BasePath>;
|
|
389
|
+
bindRoute(opts: { configs: IRpcMetadata }): IGrpcBindRouteOptions<RouteEnv>;
|
|
390
|
+
defineRoute(opts: {
|
|
391
|
+
configs: IRpcMetadata;
|
|
392
|
+
handler: TRpcHandler<unknown, unknown, RouteEnv>;
|
|
393
|
+
}): IGrpcDefineRouteOptions;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `IConnectAdapterResult`
|
|
398
|
+
|
|
399
|
+
Return type from `GrpcRequestAdapter.build()`.
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
interface IConnectAdapterResult<
|
|
403
|
+
RouteEnv extends Env = Env,
|
|
404
|
+
BasePath extends string = '/',
|
|
405
|
+
RouteInput extends Input = {},
|
|
406
|
+
> {
|
|
407
|
+
paths: string[];
|
|
408
|
+
middleware: MiddlewareHandler<RouteEnv, BasePath, RouteInput>;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## `GrpcRequestAdapter`
|
|
413
|
+
|
|
414
|
+
Internal bridge between Ignis gRPC controllers and ConnectRPC's universal handler system. You do not interact with this class directly -- it is created automatically during `configure()`.
|
|
415
|
+
|
|
416
|
+
### Architecture
|
|
417
|
+
|
|
418
|
+
The adapter solves a key challenge: ConnectRPC handlers have their own `(request, context) => response` signature, but Ignis controllers need access to the Hono `Context` for middleware, auth, and request-scoped state. The adapter uses `AsyncLocalStorage` to provide request-scoped context isolation, ensuring concurrent requests never share state.
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
Hono Request
|
|
422
|
+
-> GrpcRequestAdapter middleware (path matching via basePath + controllerPath)
|
|
423
|
+
-> AsyncLocalStorage.run(honoContext, ...)
|
|
424
|
+
-> Pre-built auth middlewares (authenticate -> authorize)
|
|
425
|
+
-> ConnectRPC universal handler
|
|
426
|
+
-> Ignis TRpcHandler (reads context from AsyncLocalStorage)
|
|
427
|
+
-> Response
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Static `build()` Method
|
|
431
|
+
|
|
432
|
+
The only public API. Validates peer deps via `validateModule()`, creates the adapter, and returns the middleware + registered paths:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
static async build(opts: {
|
|
436
|
+
controller: AbstractGrpcController<...>;
|
|
437
|
+
interceptors?: unknown[];
|
|
438
|
+
}): Promise<IConnectAdapterResult<RouteEnv, BasePath>>
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Called internally by `AbstractGrpcController.configure()`:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
const adapter = await GrpcRequestAdapter.build({ controller: this });
|
|
445
|
+
this.router.use('*', adapter.middleware);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The optional `interceptors` array is passed to ConnectRPC's `createConnectRouter()` for request/response interception at the protocol level.
|
|
449
|
+
|
|
450
|
+
### Internal Flow
|
|
451
|
+
|
|
452
|
+
1. **`buildConnectHandlers()`** -- Wraps each Ignis `TRpcHandler` into ConnectRPC's `(request, context) => response` signature. The wrapper reads the Hono context from `AsyncLocalStorage`, runs pre-built auth middlewares (built by `AbstractGrpcController.buildRpcMiddlewares()`), then passes `{ request, context }` to the Ignis handler.
|
|
453
|
+
|
|
454
|
+
2. **`registerService()`** -- Bridges the opaque `ServiceType` from `@controller` metadata to ConnectRPC's `router.service()` call, registering all handlers for the service.
|
|
455
|
+
|
|
456
|
+
3. **`buildMiddleware()`** -- Creates a Hono middleware that:
|
|
457
|
+
- Strips the full mount prefix (`basePath + controllerPath`) from the request URL to derive the ConnectRPC handler path (e.g., `/package.Service/Method`)
|
|
458
|
+
- Looks up the ConnectRPC handler by path from the handler map
|
|
459
|
+
- Runs the handler inside `AsyncLocalStorage.run()` with the current Hono context
|
|
460
|
+
- Converts between Fetch API `Request`/`Response` and ConnectRPC's `UniversalServerRequest`/`UniversalServerResponse` formats
|
|
461
|
+
- Returns proper gRPC error responses on failure (with `grpc-status` and `grpc-message` headers)
|
|
462
|
+
|
|
463
|
+
### Peer Dependency Loading
|
|
464
|
+
|
|
465
|
+
The adapter loads ConnectRPC modules at runtime using `createRequire` from the application's `node_modules`:
|
|
466
|
+
|
|
467
|
+
- `@connectrpc/connect` -- for `createConnectRouter`
|
|
468
|
+
- `@connectrpc/connect/protocol` -- for `universalServerRequestFromFetch` and `universalServerResponseToFetch`
|
|
469
|
+
|
|
470
|
+
This approach supports single-file builds where the peer deps may not be resolvable via standard `import`.
|
|
471
|
+
|
|
472
|
+
### Error Handling
|
|
473
|
+
|
|
474
|
+
On handler errors, the adapter returns a JSON response with:
|
|
475
|
+
- HTTP status: `200` if gRPC status is `OK`, `500` otherwise
|
|
476
|
+
- `grpc-status` header: Preserved from `ConnectError.code` if available (duck-type check on `error.code` being a number), otherwise `13` (INTERNAL)
|
|
477
|
+
- `grpc-message` header: URL-encoded error message
|
|
478
|
+
- Body: JSON `{ message, code }`
|
|
479
|
+
|
|
480
|
+
The adapter uses a duck-type check on `error.code` to preserve gRPC status codes from ConnectRPC errors without importing `ConnectError` directly, avoiding tight coupling to the peer dependency.
|
|
481
|
+
|
|
482
|
+
## `GrpcComponent`
|
|
483
|
+
|
|
484
|
+
Auto-discovers and configures gRPC controllers during the application lifecycle.
|
|
485
|
+
|
|
486
|
+
### Configuration
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
interface IGrpcComponentConfig {
|
|
490
|
+
interceptors?: unknown[];
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The component registers a default (empty) config binding under the key `'@app/grpc/options'` (`GrpcBindingKeys.GRPC_COMPONENT_OPTIONS`).
|
|
495
|
+
|
|
496
|
+
### Behavior
|
|
497
|
+
|
|
498
|
+
1. Finds all bindings tagged with the `controllers` namespace
|
|
499
|
+
2. Filters to controllers whose metadata has `transport: 'grpc'`
|
|
500
|
+
3. Validates each gRPC controller:
|
|
501
|
+
- If `path` is missing, throws an error
|
|
502
|
+
- If `service` is missing, logs a warning and skips the controller
|
|
503
|
+
4. Sets `instance.basePath` from the application's `path.base` config (needed for correct path stripping in the adapter)
|
|
504
|
+
5. Calls `configure()` on each controller instance
|
|
505
|
+
6. Mounts the controller's router on the application's root router at the controller's path via `router.route(metadata.path, instance.getRouter())`
|
|
506
|
+
|
|
507
|
+
### Dynamic Discovery
|
|
508
|
+
|
|
509
|
+
The component uses a re-fetch loop with `Set` tracking. After configuring each controller, it re-queries the container for new controller bindings (excluding already-configured ones). This handles controllers registered dynamically during component composition (e.g., a component that registers another component that registers a gRPC controller).
|
|
510
|
+
|
|
511
|
+
### Automatic Registration
|
|
512
|
+
|
|
513
|
+
`GrpcComponent` is instantiated and configured automatically by `BaseApplication` when `appConfigs.transports` includes `ControllerTransports.GRPC`. You do not need to register it manually. The relevant code in `BaseApplication`:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
case ControllerTransports.GRPC: {
|
|
517
|
+
const grpcComponent = new GrpcComponent(this);
|
|
518
|
+
await grpcComponent.configure();
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## `GRPC` Constants
|
|
524
|
+
|
|
525
|
+
The `GRPC` class from `@venizia/ignis-helpers` provides all gRPC protocol constants:
|
|
526
|
+
|
|
527
|
+
### Methods
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
GRPC.Methods.UNARY // 'unary'
|
|
531
|
+
GRPC.Methods.SERVER_STREAMING // 'server_streaming'
|
|
532
|
+
GRPC.Methods.CLIENT_STREAMING // 'client_streaming'
|
|
533
|
+
GRPC.Methods.BIDI_STREAMING // 'bidi_streaming'
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Result Codes
|
|
537
|
+
|
|
538
|
+
Standard gRPC status codes (matching `google.rpc.Code`):
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
GRPC.ResultCodes.OK // 0
|
|
542
|
+
GRPC.ResultCodes.CANCELLED // 1
|
|
543
|
+
GRPC.ResultCodes.UNKNOWN // 2
|
|
544
|
+
GRPC.ResultCodes.INVALID_ARGUMENT // 3
|
|
545
|
+
GRPC.ResultCodes.DEADLINE_EXCEEDED // 4
|
|
546
|
+
GRPC.ResultCodes.NOT_FOUND // 5
|
|
547
|
+
GRPC.ResultCodes.ALREADY_EXISTS // 6
|
|
548
|
+
GRPC.ResultCodes.PERMISSION_DENIED // 7
|
|
549
|
+
GRPC.ResultCodes.RESOURCE_EXHAUSTED // 8
|
|
550
|
+
GRPC.ResultCodes.FAILED_PRECONDITION // 9
|
|
551
|
+
GRPC.ResultCodes.ABORTED // 10
|
|
552
|
+
GRPC.ResultCodes.OUT_OF_RANGE // 11
|
|
553
|
+
GRPC.ResultCodes.UNIMPLEMENTED // 12
|
|
554
|
+
GRPC.ResultCodes.INTERNAL // 13
|
|
555
|
+
GRPC.ResultCodes.UNAVAILABLE // 14
|
|
556
|
+
GRPC.ResultCodes.DATA_LOSS // 15
|
|
557
|
+
GRPC.ResultCodes.UNAUTHENTICATED // 16
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Headers
|
|
561
|
+
|
|
562
|
+
Standard gRPC protocol headers:
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
GRPC.Headers.GRPC_STATUS // 'grpc-status'
|
|
566
|
+
GRPC.Headers.GRPC_MESSAGE // 'grpc-message'
|
|
567
|
+
GRPC.Headers.GRPC_TIMEOUT // 'grpc-timeout'
|
|
568
|
+
GRPC.Headers.GRPC_ENCODING // 'grpc-encoding'
|
|
569
|
+
// ... and more (see packages/helpers/src/common/constants/grpc.ts)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Content Types
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
GRPC.HeaderValues.GRPC // 'application/grpc'
|
|
576
|
+
GRPC.HeaderValues.GRPC_PROTO // 'application/grpc+proto'
|
|
577
|
+
GRPC.HeaderValues.GRPC_JSON // 'application/grpc+json'
|
|
578
|
+
GRPC.HeaderValues.GRPC_WEB // 'application/grpc-web'
|
|
579
|
+
GRPC.HeaderValues.GRPC_WEB_PROTO // 'application/grpc-web+proto'
|
|
580
|
+
GRPC.HeaderValues.GRPC_WEB_JSON // 'application/grpc-web+json'
|
|
581
|
+
GRPC.HeaderValues.GRPC_WEB_TEXT // 'application/grpc-web-text'
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Application Setup
|
|
585
|
+
|
|
586
|
+
### Enabling gRPC Transport
|
|
587
|
+
|
|
588
|
+
Add `ControllerTransports.GRPC` to the `transports` array in your application configs:
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import {
|
|
592
|
+
BaseApplication,
|
|
593
|
+
ControllerTransports,
|
|
594
|
+
IApplicationConfigs,
|
|
595
|
+
IApplicationInfo,
|
|
596
|
+
} from '@venizia/ignis';
|
|
597
|
+
import { ValueOrPromise } from '@venizia/ignis-helpers';
|
|
598
|
+
import { GreeterController } from './controllers/greeter';
|
|
599
|
+
import { GreeterService } from './services/greeter.service';
|
|
600
|
+
|
|
601
|
+
export const appConfigs: IApplicationConfigs = {
|
|
602
|
+
host: '0.0.0.0',
|
|
603
|
+
port: 3000,
|
|
604
|
+
path: { base: '/', isStrict: false },
|
|
605
|
+
transports: [ControllerTransports.REST, ControllerTransports.GRPC],
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
export class Application extends BaseApplication {
|
|
609
|
+
getAppInfo(): ValueOrPromise<IApplicationInfo> {
|
|
610
|
+
return { name: 'my-app', version: '1.0.0', description: 'gRPC + REST app' };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
staticConfigure() {}
|
|
614
|
+
|
|
615
|
+
preConfigure() {
|
|
616
|
+
this.service(GreeterService);
|
|
617
|
+
this.controller(GreeterController);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
postConfigure() {}
|
|
621
|
+
setupMiddlewares() {}
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
> [!WARNING]
|
|
626
|
+
> If `transports` does not include `ControllerTransports.GRPC`, gRPC controllers are still registered in the DI container but the `GrpcComponent` is never mounted -- their `configure()` is never called and no routes are served.
|
|
627
|
+
|
|
628
|
+
### Dual Transport
|
|
629
|
+
|
|
630
|
+
REST and gRPC controllers coexist in the same application. Each controller declares its own transport via the `@controller` decorator. A single application can serve both:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
preConfigure() {
|
|
634
|
+
// gRPC controller
|
|
635
|
+
this.controller(GreeterController);
|
|
636
|
+
|
|
637
|
+
// REST controller
|
|
638
|
+
this.controller(StatusController);
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
REST controllers are handled by the `RestComponent` (active when transports includes `ControllerTransports.REST`, which is the default); gRPC controllers are handled by the `GrpcComponent` (active when transport is enabled). They share the same DI container and lifecycle.
|
|
643
|
+
|
|
644
|
+
## Complete Example
|
|
645
|
+
|
|
646
|
+
### 1. Proto File
|
|
647
|
+
|
|
648
|
+
```protobuf
|
|
649
|
+
// proto/greeter.proto
|
|
650
|
+
syntax = "proto3";
|
|
651
|
+
package greeter.v1;
|
|
652
|
+
|
|
653
|
+
message SayHelloRequest {
|
|
654
|
+
string name = 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
message SayHelloResponse {
|
|
658
|
+
string message = 1;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
message ListUsersRequest {}
|
|
662
|
+
|
|
663
|
+
message ListUsersResponse {
|
|
664
|
+
repeated string users = 1;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
service GreeterService {
|
|
668
|
+
rpc SayHello (SayHelloRequest) returns (SayHelloResponse);
|
|
669
|
+
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 2. Generate TypeScript Code
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
buf generate proto/greeter.proto
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### 3. Definition File (Stable Import Boundary)
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// controllers/greeter/definition.ts
|
|
683
|
+
export {
|
|
684
|
+
GreeterService,
|
|
685
|
+
ListUsersResponseSchema,
|
|
686
|
+
SayHelloResponseSchema,
|
|
687
|
+
type ListUsersRequest,
|
|
688
|
+
type ListUsersResponse,
|
|
689
|
+
type SayHelloRequest,
|
|
690
|
+
type SayHelloResponse,
|
|
691
|
+
} from './generated/greeter_pb';
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
> [!TIP]
|
|
695
|
+
> Always re-export generated code through a `definition.ts` file. This acts as a stable import boundary -- controller code and external consumers import from here, never from the `generated/` directory directly. When you regenerate protos, only this file needs updating.
|
|
696
|
+
|
|
697
|
+
### 4. Controller
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// controllers/greeter/controller.ts
|
|
701
|
+
import { GreeterService } from '@/services';
|
|
702
|
+
import { create } from '@bufbuild/protobuf';
|
|
703
|
+
import {
|
|
704
|
+
BaseGrpcController,
|
|
705
|
+
ControllerTransports,
|
|
706
|
+
controller,
|
|
707
|
+
inject,
|
|
708
|
+
unary,
|
|
709
|
+
} from '@venizia/ignis';
|
|
710
|
+
import {
|
|
711
|
+
GreeterService as GreeterServiceDef,
|
|
712
|
+
ListUsersResponseSchema,
|
|
713
|
+
SayHelloResponseSchema,
|
|
714
|
+
type ListUsersRequest,
|
|
715
|
+
type ListUsersResponse,
|
|
716
|
+
type SayHelloRequest,
|
|
717
|
+
type SayHelloResponse,
|
|
718
|
+
} from './definition';
|
|
719
|
+
|
|
720
|
+
@controller({
|
|
721
|
+
path: '/grpc',
|
|
722
|
+
transport: ControllerTransports.GRPC,
|
|
723
|
+
service: GreeterServiceDef,
|
|
724
|
+
})
|
|
725
|
+
export class GreeterController extends BaseGrpcController {
|
|
726
|
+
constructor(
|
|
727
|
+
@inject({ key: 'services.GreeterService' })
|
|
728
|
+
private readonly greeterService: GreeterService,
|
|
729
|
+
) {
|
|
730
|
+
super({ scope: 'GreeterController', path: '/grpc' });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
override binding() {}
|
|
734
|
+
|
|
735
|
+
@unary({ configs: { name: 'sayHello' } })
|
|
736
|
+
async sayHello(opts: { request: SayHelloRequest }): Promise<SayHelloResponse> {
|
|
737
|
+
const message = await this.greeterService.sayHello(opts);
|
|
738
|
+
return create(SayHelloResponseSchema, { message });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
@unary({ configs: { name: 'listUsers' } })
|
|
742
|
+
async listUsers(opts: { request: ListUsersRequest }): Promise<ListUsersResponse> {
|
|
743
|
+
const users = await this.greeterService.listUsers(opts);
|
|
744
|
+
return create(ListUsersResponseSchema, { users });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### 5. Minimal Controller (No DI)
|
|
750
|
+
|
|
751
|
+
A controller with no injected dependencies:
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
// controllers/echo/controller.ts
|
|
755
|
+
import { create } from '@bufbuild/protobuf';
|
|
756
|
+
import {
|
|
757
|
+
BaseGrpcController,
|
|
758
|
+
ControllerTransports,
|
|
759
|
+
controller,
|
|
760
|
+
unary,
|
|
761
|
+
} from '@venizia/ignis';
|
|
762
|
+
import {
|
|
763
|
+
EchoResponseSchema,
|
|
764
|
+
EchoService as EchoServiceDef,
|
|
765
|
+
type EchoRequest,
|
|
766
|
+
type EchoResponse,
|
|
767
|
+
} from './definition';
|
|
768
|
+
|
|
769
|
+
@controller({
|
|
770
|
+
path: '/grpc',
|
|
771
|
+
transport: ControllerTransports.GRPC,
|
|
772
|
+
service: EchoServiceDef,
|
|
773
|
+
})
|
|
774
|
+
export class EchoController extends BaseGrpcController {
|
|
775
|
+
constructor() {
|
|
776
|
+
super({ scope: 'EchoController', path: '/grpc' });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
override binding() {}
|
|
780
|
+
|
|
781
|
+
@unary({ configs: { name: 'echo' } })
|
|
782
|
+
async echo(opts: { request: EchoRequest }): Promise<EchoResponse> {
|
|
783
|
+
return create(EchoResponseSchema, {
|
|
784
|
+
message: `Echo: ${opts.request.message}`,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 6. Application
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
// application.ts
|
|
794
|
+
import {
|
|
795
|
+
BaseApplication,
|
|
796
|
+
ControllerTransports,
|
|
797
|
+
IApplicationConfigs,
|
|
798
|
+
IApplicationInfo,
|
|
799
|
+
} from '@venizia/ignis';
|
|
800
|
+
import { ValueOrPromise } from '@venizia/ignis-helpers';
|
|
801
|
+
import { GreeterController } from './controllers/greeter';
|
|
802
|
+
import { EchoController } from './controllers/echo';
|
|
803
|
+
import { GreeterService } from './services/greeter.service';
|
|
804
|
+
|
|
805
|
+
export const appConfigs: IApplicationConfigs = {
|
|
806
|
+
host: '0.0.0.0',
|
|
807
|
+
port: 3000,
|
|
808
|
+
path: { base: '/', isStrict: false },
|
|
809
|
+
transports: [ControllerTransports.REST, ControllerTransports.GRPC],
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
export class Application extends BaseApplication {
|
|
813
|
+
getAppInfo(): ValueOrPromise<IApplicationInfo> {
|
|
814
|
+
return { name: 'greeter-app', version: '1.0.0', description: 'gRPC greeter' };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
staticConfigure() {}
|
|
818
|
+
|
|
819
|
+
preConfigure() {
|
|
820
|
+
this.service(GreeterService);
|
|
821
|
+
this.controller(GreeterController);
|
|
822
|
+
this.controller(EchoController);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
postConfigure() {}
|
|
826
|
+
setupMiddlewares() {}
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### 7. Client (Testing)
|
|
831
|
+
|
|
832
|
+
```typescript
|
|
833
|
+
// client.ts
|
|
834
|
+
import { create } from '@bufbuild/protobuf';
|
|
835
|
+
import { createClient } from '@connectrpc/connect';
|
|
836
|
+
import { createConnectTransport } from '@connectrpc/connect-web';
|
|
837
|
+
import { GreeterService, SayHelloRequestSchema } from './controllers/greeter/definition';
|
|
838
|
+
|
|
839
|
+
const transport = createConnectTransport({ baseUrl: 'http://localhost:3000/grpc' });
|
|
840
|
+
const client = createClient(GreeterService, transport);
|
|
841
|
+
|
|
842
|
+
const response = await client.sayHello(create(SayHelloRequestSchema, { name: 'Ignis' }));
|
|
843
|
+
console.log(response.message);
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
## Component-Based Registration
|
|
847
|
+
|
|
848
|
+
gRPC controllers can be registered through components, following the same pattern as REST controllers. This enables modular composition and late registration.
|
|
849
|
+
|
|
850
|
+
### Basic Component
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
import {
|
|
854
|
+
BaseApplication,
|
|
855
|
+
BaseComponent,
|
|
856
|
+
CoreBindings,
|
|
857
|
+
inject,
|
|
858
|
+
} from '@venizia/ignis';
|
|
859
|
+
import { ValueOrPromise } from '@venizia/ignis-helpers';
|
|
860
|
+
import { EchoController } from '../controllers/echo';
|
|
861
|
+
|
|
862
|
+
export class EchoComponent extends BaseComponent {
|
|
863
|
+
constructor(
|
|
864
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
|
|
865
|
+
private application: BaseApplication,
|
|
866
|
+
) {
|
|
867
|
+
super({ scope: 'EchoComponent' });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
override binding(): ValueOrPromise<void> {
|
|
871
|
+
this.application.controller(EchoController);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### Component Composition
|
|
877
|
+
|
|
878
|
+
Components can compose other components, building a dependency graph of controllers:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
import {
|
|
882
|
+
BaseApplication,
|
|
883
|
+
BaseComponent,
|
|
884
|
+
CoreBindings,
|
|
885
|
+
inject,
|
|
886
|
+
} from '@venizia/ignis';
|
|
887
|
+
import { TimeController } from '../controllers/time';
|
|
888
|
+
import { EchoComponent } from './echo.component';
|
|
889
|
+
|
|
890
|
+
export class TimeComponent extends BaseComponent {
|
|
891
|
+
constructor(
|
|
892
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
|
|
893
|
+
private application: BaseApplication,
|
|
894
|
+
) {
|
|
895
|
+
super({ scope: 'TimeComponent' });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
override async binding(): Promise<void> {
|
|
899
|
+
// Compose EchoComponent -- registers EchoController
|
|
900
|
+
this.application.component(EchoComponent);
|
|
901
|
+
|
|
902
|
+
// Register this component's own controller
|
|
903
|
+
this.application.controller(TimeController);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
The `GrpcComponent` handles these dynamically-registered controllers through its re-fetch loop -- after configuring each controller, it re-queries the container for newly added bindings.
|
|
909
|
+
|
|
910
|
+
### Registration in Application
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
preConfigure() {
|
|
914
|
+
// TimeComponent composes EchoComponent internally
|
|
915
|
+
this.component(TimeComponent);
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
## Authentication and Authorization
|
|
920
|
+
|
|
921
|
+
Per-RPC authentication and authorization are configured via the `authenticate` and `authorize` fields in `IRpcMetadata`. Auth middlewares are pre-built during route registration by `AbstractGrpcController.buildRpcMiddlewares()` and executed before the handler inside the `AsyncLocalStorage` context.
|
|
922
|
+
|
|
923
|
+
### Per-RPC Authentication
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
@unary({
|
|
927
|
+
configs: {
|
|
928
|
+
name: 'sayHello',
|
|
929
|
+
authenticate: {
|
|
930
|
+
strategies: ['jwt'],
|
|
931
|
+
mode: 'required',
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
})
|
|
935
|
+
async sayHello(opts: { request: SayHelloRequest }): Promise<SayHelloResponse> {
|
|
936
|
+
// Only accessible with a valid JWT token
|
|
937
|
+
return create(SayHelloResponseSchema, { message: 'Hello!' });
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
| Field | Type | Default | Description |
|
|
942
|
+
|-------|------|---------|-------------|
|
|
943
|
+
| `strategies` | `TAuthStrategy[]` | `[]` | Authentication strategies to apply (e.g., `['jwt']`, `['basic']`) |
|
|
944
|
+
| `mode` | `TAuthMode` | `'any'` | `'required'` \| `'optional'` \| `'any'` \| `'all'` (defaults to `AuthenticationModes.ANY`) |
|
|
945
|
+
|
|
946
|
+
### Per-RPC Authorization
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
@unary({
|
|
950
|
+
configs: {
|
|
951
|
+
name: 'deleteUser',
|
|
952
|
+
authenticate: { strategies: ['jwt'], mode: 'required' },
|
|
953
|
+
authorize: { action: 'delete', resource: 'user' },
|
|
954
|
+
},
|
|
955
|
+
})
|
|
956
|
+
async deleteUser(opts: { request: DeleteUserRequest }): Promise<DeleteUserResponse> {
|
|
957
|
+
// Requires JWT + delete permission on user resource
|
|
958
|
+
// ...
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
Multiple authorization specs can be provided as an array:
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
authorize: [
|
|
966
|
+
{ action: 'read', resource: 'user' },
|
|
967
|
+
{ action: 'read', resource: 'profile' },
|
|
968
|
+
]
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Middleware Execution Order
|
|
972
|
+
|
|
973
|
+
Auth middlewares run in the following order inside the ConnectRPC handler wrapper:
|
|
974
|
+
|
|
975
|
+
1. **Authenticate** middlewares (if `configs.authenticate.strategies` has entries)
|
|
976
|
+
2. **Authorize** middlewares (if `configs.authorize` is present), one per spec
|
|
977
|
+
3. **Handler** execution
|
|
978
|
+
|
|
979
|
+
## See Also
|
|
980
|
+
|
|
981
|
+
- [Controllers Reference](./controllers.md) -- REST controller classes and API endpoint patterns
|
|
982
|
+
- [Components Reference](./components.md) -- Component system and built-in components
|
|
983
|
+
- [Dependency Injection](./dependency-injection.md) -- IoC container, `@inject`, binding keys
|
|
984
|
+
- [Services](./services.md) -- Business logic layer
|