@bluealba/platform-cli 1.0.1 → 1.1.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/index.js +278 -15
- package/docs/404.mdx +5 -0
- package/docs/architecture/api-explorer.mdx +478 -0
- package/docs/architecture/architecture-diagrams.mdx +12 -0
- package/docs/architecture/authentication-system.mdx +903 -0
- package/docs/architecture/authorization-system.mdx +886 -0
- package/docs/architecture/bootstrap.mdx +1442 -0
- package/docs/architecture/gateway-architecture.mdx +845 -0
- package/docs/architecture/multi-tenancy.mdx +1150 -0
- package/docs/architecture/overview.mdx +776 -0
- package/docs/architecture/scheduler.mdx +818 -0
- package/docs/architecture/shell.mdx +885 -0
- package/docs/architecture/ui-extension-points.mdx +781 -0
- package/docs/architecture/user-states.mdx +794 -0
- package/docs/development/overview.mdx +21 -0
- package/docs/development/workflow.mdx +914 -0
- package/docs/getting-started/core-concepts.mdx +892 -0
- package/docs/getting-started/installation.mdx +780 -0
- package/docs/getting-started/overview.mdx +83 -0
- package/docs/getting-started/quick-start.mdx +940 -0
- package/docs/guides/adding-documentation-sites.mdx +1367 -0
- package/docs/guides/creating-services.mdx +1736 -0
- package/docs/guides/creating-ui-modules.mdx +1860 -0
- package/docs/guides/identity-providers.mdx +1007 -0
- package/docs/guides/mermaid-diagrams.mdx +212 -0
- package/docs/guides/using-feature-flags.mdx +1059 -0
- package/docs/guides/working-with-rooms.mdx +566 -0
- package/docs/index.mdx +57 -0
- package/docs/platform-cli/commands.mdx +604 -0
- package/docs/platform-cli/overview.mdx +195 -0
- package/package.json +5 -2
- package/skills/ba-platform/platform-cli.skill.md +26 -0
- package/skills/ba-platform/platform.skill.md +35 -0
- package/templates/application-monorepo-template/gitignore +95 -0
- package/templates/bootstrap-service-template/Dockerfile.development +1 -1
- package/templates/bootstrap-service-template/gitignore +57 -0
- package/templates/bootstrap-service-template/package.json +1 -1
- package/templates/bootstrap-service-template/src/main.ts +6 -16
- package/templates/customization-ui-module-template/Dockerfile.development +1 -1
- package/templates/customization-ui-module-template/gitignore +73 -0
- package/templates/nestjs-service-module-template/Dockerfile.development +1 -1
- package/templates/nestjs-service-module-template/gitignore +56 -0
- package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
- package/templates/platform-init-template/{{platformName}}-core/local/.env.example +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/platform-docker-compose.yml +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/{{platformName}}-core-docker-compose.yml +0 -1
- package/templates/react-ui-module-template/Dockerfile +1 -1
- package/templates/react-ui-module-template/Dockerfile.development +1 -3
- package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
- package/templates/react-ui-module-template/gitignore +72 -0
- package/templates/react-ui-module-template/Dockerfile_nginx +0 -11
- package/templates/react-ui-module-template/nginx/default.conf +0 -23
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Creating Backend Services
|
|
3
|
+
description: Complete guide to creating NestJS-based backend services for the Blue Alba Platform
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
import MermaidDiagram from '~/components/MermaidDiagram.astro';
|
|
8
|
+
|
|
9
|
+
The Blue Alba Platform uses a microservices architecture with NestJS + Fastify as the primary framework for building high-performance, scalable backend services. This guide walks you through everything you need to create production-ready services that integrate seamlessly with the platform.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Backend services in the Blue Alba Platform are independent, self-contained applications that handle specific business domains. They communicate with the gateway, integrate with databases, publish events, and provide RESTful APIs for frontend applications.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
The platform's microservices architecture uses a gateway pattern to route requests, handle authentication, and aggregate API documentation.
|
|
20
|
+
|
|
21
|
+
<MermaidDiagram
|
|
22
|
+
title="Microservices Architecture"
|
|
23
|
+
code={`graph TB
|
|
24
|
+
A[Client Applications<br/>React UI Modules]:::client --> B[API Gateway<br/>pae-nestjs-gateway-service]:::gateway
|
|
25
|
+
|
|
26
|
+
B --> C[Application Catalog]:::catalog
|
|
27
|
+
B --> D[Authentication]:::auth
|
|
28
|
+
B --> E[Authorization]:::authz
|
|
29
|
+
|
|
30
|
+
B --> F[Microservice 1<br/>Habits Service]:::service
|
|
31
|
+
B --> G[Microservice 2<br/>Custom Service]:::service
|
|
32
|
+
B --> H[Microservice N<br/>...]:::service
|
|
33
|
+
|
|
34
|
+
F --> I[(PostgreSQL)]:::database
|
|
35
|
+
G --> I
|
|
36
|
+
H --> I
|
|
37
|
+
|
|
38
|
+
F --> J[Kafka<br/>Event Bus]:::kafka
|
|
39
|
+
G --> J
|
|
40
|
+
H --> J
|
|
41
|
+
|
|
42
|
+
K[pae-service-nestjs-sdk<br/>Service SDK]:::sdk --> F
|
|
43
|
+
K --> G
|
|
44
|
+
K --> H
|
|
45
|
+
|
|
46
|
+
L[pae-core<br/>Domain Entities]:::core --> B
|
|
47
|
+
L --> F
|
|
48
|
+
L --> G
|
|
49
|
+
L --> H
|
|
50
|
+
|
|
51
|
+
classDef client fill:#90EE90,color:#333
|
|
52
|
+
classDef gateway fill:#FFD700,color:#333
|
|
53
|
+
classDef catalog fill:#87CEEB,color:#333
|
|
54
|
+
classDef auth fill:#DDA0DD,color:#333
|
|
55
|
+
classDef authz fill:#FFB6C1,color:#333
|
|
56
|
+
classDef service fill:#87CEEB,color:#333
|
|
57
|
+
classDef database fill:#F0E68C,color:#333
|
|
58
|
+
classDef kafka fill:#FFA07A,color:#333
|
|
59
|
+
classDef sdk fill:#ADD8E6,color:#333
|
|
60
|
+
classDef core fill:#FFB6C1,color:#333`}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
### Architecture Components
|
|
64
|
+
|
|
65
|
+
- **API Gateway**: Routes requests, handles authentication/authorization, aggregates API docs
|
|
66
|
+
- **Microservices**: Domain-specific services (NestJS or Express) handling business logic
|
|
67
|
+
- **Application Catalog**: Registry of all services and their routes
|
|
68
|
+
- **Database**: PostgreSQL for persistence (shared or per-service)
|
|
69
|
+
- **PAE Service SDK**: NestJS SDK providing common utilities and platform integration
|
|
70
|
+
- **PAE Core**: Shared domain entities, types, and authorization logic
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
Create your first NestJS service in these steps:
|
|
77
|
+
|
|
78
|
+
### 1. Install NestJS CLI
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install -g @nestjs/cli
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Create New Service
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Create new NestJS project
|
|
88
|
+
nest new my-service
|
|
89
|
+
|
|
90
|
+
# Navigate to project
|
|
91
|
+
cd my-service
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
When prompted, choose **npm** as the package manager.
|
|
95
|
+
|
|
96
|
+
### 3. Install Dependencies
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Install platform dependencies
|
|
100
|
+
npm install @bluealba/pae-service-nestjs-sdk
|
|
101
|
+
npm install @bluealba/pae-core
|
|
102
|
+
|
|
103
|
+
# Install NestJS core dependencies
|
|
104
|
+
npm install @nestjs/common@^11.0.0
|
|
105
|
+
npm install @nestjs/core@^11.0.0
|
|
106
|
+
npm install @nestjs/config@^4.0.2
|
|
107
|
+
npm install @nestjs/platform-fastify@^11.0.0
|
|
108
|
+
|
|
109
|
+
# Install Fastify
|
|
110
|
+
npm install fastify@5.4.0
|
|
111
|
+
|
|
112
|
+
# Install validation dependencies
|
|
113
|
+
npm install class-validator class-transformer
|
|
114
|
+
|
|
115
|
+
# Install HTTP client
|
|
116
|
+
npm install @nestjs/axios
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Update TypeScript Configuration
|
|
120
|
+
|
|
121
|
+
Edit `tsconfig.json`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"compilerOptions": {
|
|
126
|
+
"module": "commonjs",
|
|
127
|
+
"declaration": true,
|
|
128
|
+
"removeComments": true,
|
|
129
|
+
"emitDecoratorMetadata": true,
|
|
130
|
+
"experimentalDecorators": true,
|
|
131
|
+
"allowSyntheticDefaultImports": true,
|
|
132
|
+
"target": "ES2021",
|
|
133
|
+
"sourceMap": true,
|
|
134
|
+
"outDir": "./dist",
|
|
135
|
+
"baseUrl": "./",
|
|
136
|
+
"incremental": true,
|
|
137
|
+
"skipLibCheck": true,
|
|
138
|
+
"strictNullChecks": false,
|
|
139
|
+
"noImplicitAny": false,
|
|
140
|
+
"strictBindCallApply": false,
|
|
141
|
+
"forceConsistentCasingInFileNames": false,
|
|
142
|
+
"noFallthroughCasesInSwitch": false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 5. Configure Entry Point (main.ts)
|
|
148
|
+
|
|
149
|
+
Replace the contents of `src/main.ts`:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
153
|
+
import { NestFactory } from '@nestjs/core';
|
|
154
|
+
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
155
|
+
import { AppModule } from './app.module';
|
|
156
|
+
|
|
157
|
+
async function bootstrap() {
|
|
158
|
+
const app = await NestFactory.create<NestFastifyApplication>(
|
|
159
|
+
AppModule,
|
|
160
|
+
new FastifyAdapter({
|
|
161
|
+
trustProxy: true,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Enable validation pipes for DTOs
|
|
166
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
167
|
+
|
|
168
|
+
await app.listen({
|
|
169
|
+
host: '0.0.0.0',
|
|
170
|
+
port: 80,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
bootstrap();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 6. Configure App Module
|
|
178
|
+
|
|
179
|
+
Update `src/app.module.ts`:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { PAEServiceModule } from '@bluealba/pae-service-nestjs-sdk';
|
|
183
|
+
import { HttpModule } from '@nestjs/axios';
|
|
184
|
+
import { Module } from '@nestjs/common';
|
|
185
|
+
import { ConfigModule } from '@nestjs/config';
|
|
186
|
+
import { AppController } from './app.controller';
|
|
187
|
+
import { AppService } from './app.service';
|
|
188
|
+
|
|
189
|
+
@Module({
|
|
190
|
+
imports: [
|
|
191
|
+
// Global configuration
|
|
192
|
+
ConfigModule.forRoot({
|
|
193
|
+
isGlobal: true,
|
|
194
|
+
}),
|
|
195
|
+
|
|
196
|
+
// HTTP client for external requests
|
|
197
|
+
HttpModule.register({
|
|
198
|
+
global: true,
|
|
199
|
+
}),
|
|
200
|
+
|
|
201
|
+
// Platform integration (health, version, changelog endpoints)
|
|
202
|
+
PAEServiceModule.forRoot({
|
|
203
|
+
healthPath: '/_/health',
|
|
204
|
+
versionPath: '/_/version',
|
|
205
|
+
changelogPath: '/_/changelog',
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
controllers: [AppController],
|
|
209
|
+
providers: [AppService],
|
|
210
|
+
})
|
|
211
|
+
export class AppModule {}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 7. Update Package Scripts
|
|
215
|
+
|
|
216
|
+
Update `package.json` scripts:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"scripts": {
|
|
221
|
+
"build": "nest build",
|
|
222
|
+
"start:dev": "nest start --watch",
|
|
223
|
+
"start:prod": "node dist/src/main",
|
|
224
|
+
"test": "jest",
|
|
225
|
+
"test:unit": "jest",
|
|
226
|
+
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
227
|
+
"test:watch": "jest --watch"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 8. Test Your Service
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Run in development mode
|
|
236
|
+
npm run start:dev
|
|
237
|
+
|
|
238
|
+
# Service should be available at http://localhost:80
|
|
239
|
+
# Health check: http://localhost:80/_/health
|
|
240
|
+
# Version info: http://localhost:80/_/version
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Project Structure
|
|
246
|
+
|
|
247
|
+
A typical NestJS service follows this structure:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
my-service/
|
|
251
|
+
├── package.json # Project configuration
|
|
252
|
+
├── tsconfig.json # TypeScript configuration
|
|
253
|
+
├── nest-cli.json # NestJS CLI configuration
|
|
254
|
+
├── jest.config.js # Jest testing configuration
|
|
255
|
+
├── Dockerfile.development # Development Docker image
|
|
256
|
+
├── Dockerfile # Production Docker image
|
|
257
|
+
└── src/
|
|
258
|
+
├── main.ts # Application entry point
|
|
259
|
+
├── app.module.ts # Root application module
|
|
260
|
+
├── app.controller.ts # Example controller
|
|
261
|
+
├── app.service.ts # Example service
|
|
262
|
+
├── config/ # Configuration files
|
|
263
|
+
│ └── configuration.ts
|
|
264
|
+
├── users/ # Feature module (example)
|
|
265
|
+
│ ├── users.module.ts
|
|
266
|
+
│ ├── users.controller.ts
|
|
267
|
+
│ ├── users.service.ts
|
|
268
|
+
│ ├── dto/
|
|
269
|
+
│ │ ├── create-user.dto.ts
|
|
270
|
+
│ │ └── update-user.dto.ts
|
|
271
|
+
│ └── entities/
|
|
272
|
+
│ └── user.entity.ts
|
|
273
|
+
├── common/ # Shared utilities
|
|
274
|
+
│ ├── guards/
|
|
275
|
+
│ ├── interceptors/
|
|
276
|
+
│ ├── filters/
|
|
277
|
+
│ └── decorators/
|
|
278
|
+
└── database/ # Database integration
|
|
279
|
+
└── database.module.ts
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Key Directories Explained
|
|
283
|
+
|
|
284
|
+
<CardGrid>
|
|
285
|
+
<Card title="src/" icon="folder">
|
|
286
|
+
Main source code directory containing all application logic.
|
|
287
|
+
</Card>
|
|
288
|
+
|
|
289
|
+
<Card title="src/[feature]/" icon="puzzle">
|
|
290
|
+
Feature modules organized by domain (users, products, orders, etc.).
|
|
291
|
+
</Card>
|
|
292
|
+
|
|
293
|
+
<Card title="dto/" icon="document">
|
|
294
|
+
Data Transfer Objects for request validation and API contracts.
|
|
295
|
+
</Card>
|
|
296
|
+
|
|
297
|
+
<Card title="entities/" icon="seti:db">
|
|
298
|
+
Domain entities representing data models and business logic.
|
|
299
|
+
</Card>
|
|
300
|
+
|
|
301
|
+
<Card title="common/" icon="setting">
|
|
302
|
+
Shared utilities like guards, interceptors, decorators, and filters.
|
|
303
|
+
</Card>
|
|
304
|
+
|
|
305
|
+
<Card title="test/" icon="approve-check">
|
|
306
|
+
E2E tests for integration testing across modules.
|
|
307
|
+
</Card>
|
|
308
|
+
</CardGrid>
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Development Workflow
|
|
313
|
+
|
|
314
|
+
### Running Locally with Docker
|
|
315
|
+
|
|
316
|
+
The recommended way to develop services is using Docker Compose, which provides a consistent environment and automatic integration with the platform.
|
|
317
|
+
|
|
318
|
+
### Project Structure Setup
|
|
319
|
+
|
|
320
|
+
Your repositories should be organized at the same level:
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
my-projects/
|
|
324
|
+
├── my-product-local/ # Local environment with docker-compose.yml
|
|
325
|
+
│ └── docker-compose.yml
|
|
326
|
+
└── my-service/ # Your service repository
|
|
327
|
+
├── Dockerfile.development
|
|
328
|
+
├── package.json
|
|
329
|
+
└── src/
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
<Aside type="note">
|
|
333
|
+
The docker-compose.yml uses `${PWD}/../my-service` to reference the service repository. This requires both repositories to be siblings in the same parent directory.
|
|
334
|
+
</Aside>
|
|
335
|
+
|
|
336
|
+
### Step 1: Create Development Dockerfile
|
|
337
|
+
|
|
338
|
+
Create `Dockerfile.development` in your service repository:
|
|
339
|
+
|
|
340
|
+
```dockerfile
|
|
341
|
+
FROM node:20-alpine as development
|
|
342
|
+
|
|
343
|
+
ENV NODE_ENV=development
|
|
344
|
+
ENV PORT=80
|
|
345
|
+
|
|
346
|
+
ARG BA_NPM_AUTH_TOKEN
|
|
347
|
+
|
|
348
|
+
WORKDIR /app
|
|
349
|
+
|
|
350
|
+
COPY package*.json ./
|
|
351
|
+
|
|
352
|
+
RUN echo "@bluealba:registry=https://bluealba.jfrog.io/artifactory/api/npm/npm-release-virtual/" > .npmrc
|
|
353
|
+
RUN echo "//bluealba.jfrog.io/artifactory/api/npm/npm-release-virtual/:_auth=${BA_NPM_AUTH_TOKEN}" >> .npmrc
|
|
354
|
+
|
|
355
|
+
RUN npm install
|
|
356
|
+
|
|
357
|
+
EXPOSE 80
|
|
358
|
+
|
|
359
|
+
CMD ["npm", "run", "start:dev"]
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Key points:**
|
|
363
|
+
- Uses Node 20 Alpine for a lightweight image
|
|
364
|
+
- Sets `NODE_ENV=development` for development mode
|
|
365
|
+
- Exposes port 80 (configurable)
|
|
366
|
+
- Runs `start:dev` command for hot reload with NestJS watch mode
|
|
367
|
+
|
|
368
|
+
### Step 2: Configure Docker Compose
|
|
369
|
+
|
|
370
|
+
Add your service to `docker-compose.yml` in your local environment repository:
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
services:
|
|
374
|
+
my-service:
|
|
375
|
+
build:
|
|
376
|
+
context: ../my-service
|
|
377
|
+
dockerfile: Dockerfile.development
|
|
378
|
+
args:
|
|
379
|
+
- BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
|
|
380
|
+
ports:
|
|
381
|
+
- 9002:80
|
|
382
|
+
environment:
|
|
383
|
+
- GATEWAY_URL=${PAE_GATEWAY_URL}
|
|
384
|
+
volumes:
|
|
385
|
+
- ${PWD}/../pae-sandbox-home-service:/app
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Configuration explained:**
|
|
389
|
+
|
|
390
|
+
| Property | Description |
|
|
391
|
+
|----------|-------------|
|
|
392
|
+
| `context: ../my-service` | Path to your service repository (relative to compose file) |
|
|
393
|
+
| `dockerfile: Dockerfile.development` | Development-specific Dockerfile |
|
|
394
|
+
| `ports: 9002:80` | Maps host port 9002 to container port 80 |
|
|
395
|
+
| `volumes` | Mounts your code for hot reload (changes reflect immediately) |
|
|
396
|
+
| `environment` | Sets environment variables (database URL, etc.) |
|
|
397
|
+
|
|
398
|
+
### Step 3: Start Development Environment
|
|
399
|
+
|
|
400
|
+
From your local environment repository:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
# Start your service
|
|
404
|
+
docker-compose up my-service
|
|
405
|
+
|
|
406
|
+
# Or start in detached mode
|
|
407
|
+
docker-compose up -d my-service
|
|
408
|
+
|
|
409
|
+
# View logs
|
|
410
|
+
docker-compose logs -f my-service
|
|
411
|
+
|
|
412
|
+
# Rebuild after dependency changes
|
|
413
|
+
docker-compose up --build my-service
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**What happens:**
|
|
417
|
+
1. Docker builds the development image
|
|
418
|
+
2. Installs npm dependencies inside the container
|
|
419
|
+
3. Starts the NestJS dev server with watch mode (`nest start --watch`)
|
|
420
|
+
4. Your code is mounted as a volume - changes trigger automatic rebuilds
|
|
421
|
+
5. Service is accessible at `http://localhost:9002`
|
|
422
|
+
|
|
423
|
+
### Step 4: Verify Service is Running
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Check health endpoint
|
|
427
|
+
curl http://localhost:9002/_/health
|
|
428
|
+
|
|
429
|
+
# Check version endpoint
|
|
430
|
+
curl http://localhost:9002/_/version
|
|
431
|
+
|
|
432
|
+
# Check changelog endpoint
|
|
433
|
+
curl http://localhost:9002/_/changelog
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Core Concepts
|
|
439
|
+
|
|
440
|
+
### Controllers
|
|
441
|
+
|
|
442
|
+
Controllers handle incoming HTTP requests and return responses to the client. They use decorators to define routes and HTTP methods.
|
|
443
|
+
|
|
444
|
+
<Tabs>
|
|
445
|
+
<TabItem label="Basic Controller">
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { Controller, Get, Post, Body, Param, Patch, Delete } from '@nestjs/common';
|
|
449
|
+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
|
450
|
+
import { UsersService } from './users.service';
|
|
451
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
452
|
+
import { UpdateUserDto } from './dto/update-user.dto';
|
|
453
|
+
|
|
454
|
+
@Controller('users')
|
|
455
|
+
@ApiTags('users')
|
|
456
|
+
export class UsersController {
|
|
457
|
+
constructor(private readonly usersService: UsersService) {}
|
|
458
|
+
|
|
459
|
+
@Post()
|
|
460
|
+
@ApiOperation({ summary: 'Create a new user' })
|
|
461
|
+
@ApiResponse({ status: 201, description: 'User created successfully' })
|
|
462
|
+
create(@Body() createUserDto: CreateUserDto) {
|
|
463
|
+
return this.usersService.create(createUserDto);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@Get()
|
|
467
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
468
|
+
findAll() {
|
|
469
|
+
return this.usersService.findAll();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@Get(':id')
|
|
473
|
+
@ApiOperation({ summary: 'Get user by ID' })
|
|
474
|
+
findOne(@Param('id') id: string) {
|
|
475
|
+
return this.usersService.findOne(+id);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@Patch(':id')
|
|
479
|
+
@ApiOperation({ summary: 'Update user by ID' })
|
|
480
|
+
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
|
481
|
+
return this.usersService.update(+id, updateUserDto);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@Delete(':id')
|
|
485
|
+
@ApiOperation({ summary: 'Delete user by ID' })
|
|
486
|
+
remove(@Param('id') id: string) {
|
|
487
|
+
return this.usersService.remove(+id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
</TabItem>
|
|
493
|
+
|
|
494
|
+
<TabItem label="Query Parameters">
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import { Controller, Get, Query } from '@nestjs/common';
|
|
498
|
+
import { ApiQuery } from '@nestjs/swagger';
|
|
499
|
+
|
|
500
|
+
@Controller('users')
|
|
501
|
+
export class UsersController {
|
|
502
|
+
constructor(private readonly usersService: UsersService) {}
|
|
503
|
+
|
|
504
|
+
@Get()
|
|
505
|
+
@ApiQuery({ name: 'page', required: false, type: Number })
|
|
506
|
+
@ApiQuery({ name: 'limit', required: false, type: Number })
|
|
507
|
+
@ApiQuery({ name: 'search', required: false, type: String })
|
|
508
|
+
findAll(
|
|
509
|
+
@Query('page') page?: string,
|
|
510
|
+
@Query('limit') limit?: string,
|
|
511
|
+
@Query('search') search?: string,
|
|
512
|
+
) {
|
|
513
|
+
return this.usersService.findAll({
|
|
514
|
+
page: page ? parseInt(page) : 1,
|
|
515
|
+
limit: limit ? parseInt(limit) : 10,
|
|
516
|
+
search,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
</TabItem>
|
|
523
|
+
|
|
524
|
+
<TabItem label="Request Headers">
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { Controller, Get, Headers, HttpCode } from '@nestjs/common';
|
|
528
|
+
|
|
529
|
+
@Controller('users')
|
|
530
|
+
export class UsersController {
|
|
531
|
+
constructor(private readonly usersService: UsersService) {}
|
|
532
|
+
|
|
533
|
+
@Get('profile')
|
|
534
|
+
@HttpCode(200)
|
|
535
|
+
getProfile(@Headers('authorization') authHeader: string) {
|
|
536
|
+
// Extract and validate token
|
|
537
|
+
const token = authHeader?.replace('Bearer ', '');
|
|
538
|
+
return this.usersService.getProfile(token);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
</TabItem>
|
|
544
|
+
|
|
545
|
+
<TabItem label="Async Operations">
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import { Controller, Get, Post, Body } from '@nestjs/common';
|
|
549
|
+
|
|
550
|
+
@Controller('users')
|
|
551
|
+
export class UsersController {
|
|
552
|
+
constructor(private readonly usersService: UsersService) {}
|
|
553
|
+
|
|
554
|
+
@Post()
|
|
555
|
+
async create(@Body() createUserDto: CreateUserDto) {
|
|
556
|
+
// Async/await for database operations
|
|
557
|
+
const user = await this.usersService.create(createUserDto);
|
|
558
|
+
|
|
559
|
+
// Trigger async tasks (email, notifications, etc.)
|
|
560
|
+
this.usersService.sendWelcomeEmail(user.email);
|
|
561
|
+
|
|
562
|
+
return user;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@Get()
|
|
566
|
+
async findAll(): Promise<User[]> {
|
|
567
|
+
return await this.usersService.findAll();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
</TabItem>
|
|
573
|
+
</Tabs>
|
|
574
|
+
|
|
575
|
+
### Services
|
|
576
|
+
|
|
577
|
+
Services contain business logic and are injected into controllers using dependency injection. They should be stateless and focused on a single responsibility.
|
|
578
|
+
|
|
579
|
+
<Tabs>
|
|
580
|
+
<TabItem label="Basic Service">
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
584
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
585
|
+
import { UpdateUserDto } from './dto/update-user.dto';
|
|
586
|
+
import { User } from './entities/user.entity';
|
|
587
|
+
|
|
588
|
+
@Injectable()
|
|
589
|
+
export class UsersService {
|
|
590
|
+
private users: User[] = [];
|
|
591
|
+
private idCounter = 1;
|
|
592
|
+
|
|
593
|
+
create(createUserDto: CreateUserDto): User {
|
|
594
|
+
const user: User = {
|
|
595
|
+
id: this.idCounter++,
|
|
596
|
+
...createUserDto,
|
|
597
|
+
createdAt: new Date(),
|
|
598
|
+
};
|
|
599
|
+
this.users.push(user);
|
|
600
|
+
return user;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
findAll(): User[] {
|
|
604
|
+
return this.users;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
findOne(id: number): User {
|
|
608
|
+
const user = this.users.find(u => u.id === id);
|
|
609
|
+
if (!user) {
|
|
610
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
611
|
+
}
|
|
612
|
+
return user;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
update(id: number, updateUserDto: UpdateUserDto): User {
|
|
616
|
+
const user = this.findOne(id);
|
|
617
|
+
Object.assign(user, updateUserDto);
|
|
618
|
+
return user;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
remove(id: number): void {
|
|
622
|
+
const index = this.users.findIndex(u => u.id === id);
|
|
623
|
+
if (index === -1) {
|
|
624
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
625
|
+
}
|
|
626
|
+
this.users.splice(index, 1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
</TabItem>
|
|
632
|
+
|
|
633
|
+
<TabItem label="Database Service">
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
637
|
+
import { DatabaseService } from '../database/database.service';
|
|
638
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
639
|
+
import { UpdateUserDto } from './dto/update-user.dto';
|
|
640
|
+
import { User } from './entities/user.entity';
|
|
641
|
+
|
|
642
|
+
@Injectable()
|
|
643
|
+
export class UsersService {
|
|
644
|
+
private readonly tableName = 'users';
|
|
645
|
+
|
|
646
|
+
constructor(private readonly dbService: DatabaseService) {}
|
|
647
|
+
|
|
648
|
+
async create(createUserDto: CreateUserDto): Promise<User> {
|
|
649
|
+
const [result] = await this.dbService.db(this.tableName)
|
|
650
|
+
.insert({
|
|
651
|
+
username: createUserDto.username,
|
|
652
|
+
email: createUserDto.email,
|
|
653
|
+
display_name: createUserDto.displayName,
|
|
654
|
+
created_at: new Date(),
|
|
655
|
+
})
|
|
656
|
+
.returning('*');
|
|
657
|
+
|
|
658
|
+
return User.fromDatabaseRow(result);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async findAll(): Promise<User[]> {
|
|
662
|
+
const result = await this.dbService.db(this.tableName)
|
|
663
|
+
.select('*')
|
|
664
|
+
.orderBy('created_at', 'desc');
|
|
665
|
+
|
|
666
|
+
return result.map(User.fromDatabaseRow);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async findOne(id: number): Promise<User> {
|
|
670
|
+
const [result] = await this.dbService.db(this.tableName)
|
|
671
|
+
.select('*')
|
|
672
|
+
.where('id', id);
|
|
673
|
+
|
|
674
|
+
if (!result) {
|
|
675
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return User.fromDatabaseRow(result);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
|
682
|
+
const [result] = await this.dbService.db(this.tableName)
|
|
683
|
+
.update({
|
|
684
|
+
username: updateUserDto.username,
|
|
685
|
+
email: updateUserDto.email,
|
|
686
|
+
display_name: updateUserDto.displayName,
|
|
687
|
+
updated_at: new Date(),
|
|
688
|
+
})
|
|
689
|
+
.where('id', id)
|
|
690
|
+
.returning('*');
|
|
691
|
+
|
|
692
|
+
if (!result) {
|
|
693
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return User.fromDatabaseRow(result);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async remove(id: number): Promise<void> {
|
|
700
|
+
const deleted = await this.dbService.db(this.tableName)
|
|
701
|
+
.delete()
|
|
702
|
+
.where('id', id);
|
|
703
|
+
|
|
704
|
+
if (!deleted) {
|
|
705
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
</TabItem>
|
|
712
|
+
|
|
713
|
+
<TabItem label="Service with Caching">
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
|
717
|
+
import { Cache } from 'cache-manager';
|
|
718
|
+
import { DatabaseService } from '../database/database.service';
|
|
719
|
+
import { User } from './entities/user.entity';
|
|
720
|
+
|
|
721
|
+
@Injectable()
|
|
722
|
+
export class UsersService {
|
|
723
|
+
constructor(
|
|
724
|
+
private readonly dbService: DatabaseService,
|
|
725
|
+
@Inject('USERS_CACHE') private readonly cache: Cache,
|
|
726
|
+
) {}
|
|
727
|
+
|
|
728
|
+
async findAll(): Promise<User[]> {
|
|
729
|
+
// Cache for 5 minutes
|
|
730
|
+
return this.cache.wrap<User[]>(
|
|
731
|
+
'all_users',
|
|
732
|
+
async () => {
|
|
733
|
+
const result = await this.dbService.db('users').select('*');
|
|
734
|
+
return result.map(User.fromDatabaseRow);
|
|
735
|
+
},
|
|
736
|
+
300000
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async findOne(id: number): Promise<User> {
|
|
741
|
+
return this.cache.wrap<User>(
|
|
742
|
+
`user_${id}`,
|
|
743
|
+
async () => {
|
|
744
|
+
const [result] = await this.dbService.db('users')
|
|
745
|
+
.select('*')
|
|
746
|
+
.where('id', id);
|
|
747
|
+
|
|
748
|
+
if (!result) {
|
|
749
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return User.fromDatabaseRow(result);
|
|
753
|
+
},
|
|
754
|
+
300000
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async invalidateCache(id?: number): Promise<void> {
|
|
759
|
+
if (id) {
|
|
760
|
+
await this.cache.del(`user_${id}`);
|
|
761
|
+
}
|
|
762
|
+
await this.cache.del('all_users');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
</TabItem>
|
|
768
|
+
|
|
769
|
+
<TabItem label="Service with Dependencies">
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
import { Injectable } from '@nestjs/common';
|
|
773
|
+
import { HttpService } from '@nestjs/axios';
|
|
774
|
+
import { firstValueFrom } from 'rxjs';
|
|
775
|
+
import { EmailService } from '../email/email.service';
|
|
776
|
+
import { LoggerService } from '../logger/logger.service';
|
|
777
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
778
|
+
|
|
779
|
+
@Injectable()
|
|
780
|
+
export class UsersService {
|
|
781
|
+
constructor(
|
|
782
|
+
private readonly httpService: HttpService,
|
|
783
|
+
private readonly emailService: EmailService,
|
|
784
|
+
private readonly logger: LoggerService,
|
|
785
|
+
) {}
|
|
786
|
+
|
|
787
|
+
async create(createUserDto: CreateUserDto) {
|
|
788
|
+
this.logger.log('Creating new user', { username: createUserDto.username });
|
|
789
|
+
|
|
790
|
+
// Create user in database
|
|
791
|
+
const user = await this.saveUser(createUserDto);
|
|
792
|
+
|
|
793
|
+
// Send welcome email (async, don't block)
|
|
794
|
+
this.emailService.sendWelcomeEmail(user.email, user.displayName)
|
|
795
|
+
.catch(err => this.logger.error('Failed to send welcome email', err));
|
|
796
|
+
|
|
797
|
+
// Notify external system (await response)
|
|
798
|
+
try {
|
|
799
|
+
const response = await firstValueFrom(
|
|
800
|
+
this.httpService.post('https://external-api.com/users', user)
|
|
801
|
+
);
|
|
802
|
+
this.logger.log('User synced to external system', response.data);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
this.logger.error('Failed to sync user to external system', error);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return user;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private async saveUser(createUserDto: CreateUserDto) {
|
|
811
|
+
// Implementation
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
</TabItem>
|
|
817
|
+
</Tabs>
|
|
818
|
+
|
|
819
|
+
### Modules
|
|
820
|
+
|
|
821
|
+
Modules organize your application into cohesive blocks of functionality. Each feature should be its own module.
|
|
822
|
+
|
|
823
|
+
<Tabs>
|
|
824
|
+
<TabItem label="Feature Module">
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
import { Module } from '@nestjs/common';
|
|
828
|
+
import { UsersController } from './users.controller';
|
|
829
|
+
import { UsersService } from './users.service';
|
|
830
|
+
|
|
831
|
+
@Module({
|
|
832
|
+
controllers: [UsersController],
|
|
833
|
+
providers: [UsersService],
|
|
834
|
+
exports: [UsersService], // Export if other modules need it
|
|
835
|
+
})
|
|
836
|
+
export class UsersModule {}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
</TabItem>
|
|
840
|
+
|
|
841
|
+
<TabItem label="Module with Dependencies">
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
import { Module } from '@nestjs/common';
|
|
845
|
+
import { DatabaseModule } from '../database/database.module';
|
|
846
|
+
import { EmailModule } from '../email/email.module';
|
|
847
|
+
import { UsersController } from './users.controller';
|
|
848
|
+
import { UsersService } from './users.service';
|
|
849
|
+
|
|
850
|
+
@Module({
|
|
851
|
+
imports: [
|
|
852
|
+
DatabaseModule, // Import other modules this module depends on
|
|
853
|
+
EmailModule,
|
|
854
|
+
],
|
|
855
|
+
controllers: [UsersController],
|
|
856
|
+
providers: [UsersService],
|
|
857
|
+
exports: [UsersService],
|
|
858
|
+
})
|
|
859
|
+
export class UsersModule {}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
</TabItem>
|
|
863
|
+
|
|
864
|
+
<TabItem label="Global Module">
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
import { Global, Module } from '@nestjs/common';
|
|
868
|
+
import { LoggerService } from './logger.service';
|
|
869
|
+
|
|
870
|
+
@Global() // Make this module available everywhere
|
|
871
|
+
@Module({
|
|
872
|
+
providers: [LoggerService],
|
|
873
|
+
exports: [LoggerService],
|
|
874
|
+
})
|
|
875
|
+
export class LoggerModule {}
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
</TabItem>
|
|
879
|
+
|
|
880
|
+
<TabItem label="Dynamic Module">
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
import { DynamicModule, Module } from '@nestjs/common';
|
|
884
|
+
|
|
885
|
+
interface DatabaseOptions {
|
|
886
|
+
host: string;
|
|
887
|
+
port: number;
|
|
888
|
+
database: string;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@Module({})
|
|
892
|
+
export class DatabaseModule {
|
|
893
|
+
static forRoot(options: DatabaseOptions): DynamicModule {
|
|
894
|
+
return {
|
|
895
|
+
module: DatabaseModule,
|
|
896
|
+
providers: [
|
|
897
|
+
{
|
|
898
|
+
provide: 'DATABASE_OPTIONS',
|
|
899
|
+
useValue: options,
|
|
900
|
+
},
|
|
901
|
+
DatabaseService,
|
|
902
|
+
],
|
|
903
|
+
exports: [DatabaseService],
|
|
904
|
+
global: true,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Usage in AppModule:
|
|
910
|
+
// DatabaseModule.forRoot({ host: 'localhost', port: 5432, database: 'mydb' })
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
</TabItem>
|
|
914
|
+
</Tabs>
|
|
915
|
+
|
|
916
|
+
### DTOs and Validation
|
|
917
|
+
|
|
918
|
+
Data Transfer Objects (DTOs) define the shape of data for requests and responses. Use `class-validator` for automatic validation.
|
|
919
|
+
|
|
920
|
+
<Tabs>
|
|
921
|
+
<TabItem label="Create DTO">
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
import { IsString, IsEmail, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
|
|
925
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
926
|
+
|
|
927
|
+
export class CreateUserDto {
|
|
928
|
+
@ApiProperty({ description: 'User username', example: 'johndoe' })
|
|
929
|
+
@IsString()
|
|
930
|
+
@IsNotEmpty()
|
|
931
|
+
@MinLength(3)
|
|
932
|
+
@MaxLength(50)
|
|
933
|
+
username: string;
|
|
934
|
+
|
|
935
|
+
@ApiProperty({ description: 'User email address', example: 'john@example.com' })
|
|
936
|
+
@IsEmail()
|
|
937
|
+
@IsNotEmpty()
|
|
938
|
+
email: string;
|
|
939
|
+
|
|
940
|
+
@ApiProperty({ description: 'User display name', example: 'John Doe' })
|
|
941
|
+
@IsString()
|
|
942
|
+
@IsNotEmpty()
|
|
943
|
+
displayName: string;
|
|
944
|
+
|
|
945
|
+
@ApiProperty({ description: 'User password', example: 'StrongP@ss123' })
|
|
946
|
+
@IsString()
|
|
947
|
+
@IsNotEmpty()
|
|
948
|
+
@MinLength(8)
|
|
949
|
+
password: string;
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
</TabItem>
|
|
954
|
+
|
|
955
|
+
<TabItem label="Update DTO">
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
import { PartialType } from '@nestjs/mapped-types';
|
|
959
|
+
import { CreateUserDto } from './create-user.dto';
|
|
960
|
+
|
|
961
|
+
// All fields optional for updates
|
|
962
|
+
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
</TabItem>
|
|
966
|
+
|
|
967
|
+
<TabItem label="Advanced Validation">
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
import {
|
|
971
|
+
IsString,
|
|
972
|
+
IsNumber,
|
|
973
|
+
IsOptional,
|
|
974
|
+
IsEnum,
|
|
975
|
+
IsArray,
|
|
976
|
+
ValidateNested,
|
|
977
|
+
Min,
|
|
978
|
+
Max
|
|
979
|
+
} from 'class-validator';
|
|
980
|
+
import { Type } from 'class-transformer';
|
|
981
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
982
|
+
|
|
983
|
+
enum UserRole {
|
|
984
|
+
ADMIN = 'admin',
|
|
985
|
+
USER = 'user',
|
|
986
|
+
GUEST = 'guest',
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
class AddressDto {
|
|
990
|
+
@IsString()
|
|
991
|
+
street: string;
|
|
992
|
+
|
|
993
|
+
@IsString()
|
|
994
|
+
city: string;
|
|
995
|
+
|
|
996
|
+
@IsString()
|
|
997
|
+
zipCode: string;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export class CreateUserDto {
|
|
1001
|
+
@ApiProperty()
|
|
1002
|
+
@IsString()
|
|
1003
|
+
username: string;
|
|
1004
|
+
|
|
1005
|
+
@ApiProperty({ enum: UserRole, default: UserRole.USER })
|
|
1006
|
+
@IsEnum(UserRole)
|
|
1007
|
+
@IsOptional()
|
|
1008
|
+
role?: UserRole = UserRole.USER;
|
|
1009
|
+
|
|
1010
|
+
@ApiProperty({ minimum: 18, maximum: 120 })
|
|
1011
|
+
@IsNumber()
|
|
1012
|
+
@Min(18)
|
|
1013
|
+
@Max(120)
|
|
1014
|
+
age: number;
|
|
1015
|
+
|
|
1016
|
+
@ApiProperty({ type: [String] })
|
|
1017
|
+
@IsArray()
|
|
1018
|
+
@IsString({ each: true })
|
|
1019
|
+
tags: string[];
|
|
1020
|
+
|
|
1021
|
+
@ApiProperty({ type: AddressDto })
|
|
1022
|
+
@ValidateNested()
|
|
1023
|
+
@Type(() => AddressDto)
|
|
1024
|
+
address: AddressDto;
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
</TabItem>
|
|
1029
|
+
|
|
1030
|
+
<TabItem label="Custom Validators">
|
|
1031
|
+
|
|
1032
|
+
```typescript
|
|
1033
|
+
import {
|
|
1034
|
+
registerDecorator,
|
|
1035
|
+
ValidationOptions,
|
|
1036
|
+
ValidatorConstraint,
|
|
1037
|
+
ValidatorConstraintInterface
|
|
1038
|
+
} from 'class-validator';
|
|
1039
|
+
|
|
1040
|
+
@ValidatorConstraint({ async: false })
|
|
1041
|
+
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
|
|
1042
|
+
validate(password: string) {
|
|
1043
|
+
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special char
|
|
1044
|
+
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
|
1045
|
+
return regex.test(password);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
defaultMessage() {
|
|
1049
|
+
return 'Password must be at least 8 characters with uppercase, lowercase, number, and special character';
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function IsStrongPassword(validationOptions?: ValidationOptions) {
|
|
1054
|
+
return function (object: Object, propertyName: string) {
|
|
1055
|
+
registerDecorator({
|
|
1056
|
+
target: object.constructor,
|
|
1057
|
+
propertyName: propertyName,
|
|
1058
|
+
options: validationOptions,
|
|
1059
|
+
constraints: [],
|
|
1060
|
+
validator: IsStrongPasswordConstraint,
|
|
1061
|
+
});
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Usage
|
|
1066
|
+
export class CreateUserDto {
|
|
1067
|
+
@IsStrongPassword()
|
|
1068
|
+
password: string;
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
</TabItem>
|
|
1073
|
+
</Tabs>
|
|
1074
|
+
|
|
1075
|
+
### Dependency Injection
|
|
1076
|
+
|
|
1077
|
+
NestJS uses dependency injection to manage dependencies between classes. This promotes loose coupling and testability.
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
// Logger Service
|
|
1081
|
+
@Injectable()
|
|
1082
|
+
export class LoggerService {
|
|
1083
|
+
log(message: string) {
|
|
1084
|
+
console.log(`[LOG]: ${message}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Users Service with dependency
|
|
1089
|
+
@Injectable()
|
|
1090
|
+
export class UsersService {
|
|
1091
|
+
constructor(
|
|
1092
|
+
private readonly logger: LoggerService, // Injected automatically
|
|
1093
|
+
) {}
|
|
1094
|
+
|
|
1095
|
+
create(user: CreateUserDto) {
|
|
1096
|
+
this.logger.log(`Creating user: ${user.username}`);
|
|
1097
|
+
// ...
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Module configuration
|
|
1102
|
+
@Module({
|
|
1103
|
+
providers: [
|
|
1104
|
+
LoggerService, // Register the service
|
|
1105
|
+
UsersService, // Can inject LoggerService
|
|
1106
|
+
],
|
|
1107
|
+
exports: [UsersService],
|
|
1108
|
+
})
|
|
1109
|
+
export class UsersModule {}
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## Platform Integration
|
|
1115
|
+
|
|
1116
|
+
### Using PAE Service SDK
|
|
1117
|
+
|
|
1118
|
+
The PAE Service SDK provides common utilities for platform integration:
|
|
1119
|
+
|
|
1120
|
+
```typescript
|
|
1121
|
+
import { PAEServiceModule } from '@bluealba/pae-service-nestjs-sdk';
|
|
1122
|
+
import { Module } from '@nestjs/common';
|
|
1123
|
+
|
|
1124
|
+
@Module({
|
|
1125
|
+
imports: [
|
|
1126
|
+
PAEServiceModule.forRoot({
|
|
1127
|
+
healthPath: '/_/health', // GET /_/health
|
|
1128
|
+
versionPath: '/_/version', // GET /_/version
|
|
1129
|
+
changelogPath: '/_/changelog', // GET /_/changelog
|
|
1130
|
+
}),
|
|
1131
|
+
],
|
|
1132
|
+
})
|
|
1133
|
+
export class AppModule {}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
This automatically provides three endpoints:
|
|
1137
|
+
|
|
1138
|
+
| Endpoint | Description | Source |
|
|
1139
|
+
|----------|-------------|--------|
|
|
1140
|
+
| `/_/health` | Returns `{ status: 'ok' }` for health checks | Built-in |
|
|
1141
|
+
| `/_/version` | Returns version from `package.json` | `package.json` |
|
|
1142
|
+
| `/_/changelog` | Returns changelog content | `CHANGELOG.md` |
|
|
1143
|
+
|
|
1144
|
+
### Swagger/OpenAPI Documentation
|
|
1145
|
+
|
|
1146
|
+
Add API documentation using the PAE Service SDK:
|
|
1147
|
+
|
|
1148
|
+
<Tabs>
|
|
1149
|
+
<TabItem label="Setup in main.ts">
|
|
1150
|
+
|
|
1151
|
+
```typescript
|
|
1152
|
+
import { NestFactory } from '@nestjs/core';
|
|
1153
|
+
import { AppModule } from './app.module';
|
|
1154
|
+
import { ApiDocsService } from '@bluealba/pae-service-nestjs-sdk';
|
|
1155
|
+
import packageJSON from '../package.json';
|
|
1156
|
+
|
|
1157
|
+
async function bootstrap() {
|
|
1158
|
+
const app = await NestFactory.create(AppModule);
|
|
1159
|
+
|
|
1160
|
+
// Setup Swagger
|
|
1161
|
+
const apiDocsService = app.get(ApiDocsService);
|
|
1162
|
+
apiDocsService.setup(app, {
|
|
1163
|
+
title: 'My Service API',
|
|
1164
|
+
description: 'API documentation for My Service',
|
|
1165
|
+
version: packageJSON.version,
|
|
1166
|
+
tags: ['Users', 'Products'],
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
await app.listen(80);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
bootstrap();
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
</TabItem>
|
|
1176
|
+
|
|
1177
|
+
<TabItem label="Add to Module">
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
import { Module } from '@nestjs/common';
|
|
1181
|
+
import { ApiDocsModule } from '@bluealba/pae-service-nestjs-sdk';
|
|
1182
|
+
import { UsersModule } from './users/users.module';
|
|
1183
|
+
|
|
1184
|
+
@Module({
|
|
1185
|
+
imports: [
|
|
1186
|
+
ApiDocsModule, // Required for API docs
|
|
1187
|
+
UsersModule,
|
|
1188
|
+
],
|
|
1189
|
+
})
|
|
1190
|
+
export class AppModule {}
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
</TabItem>
|
|
1194
|
+
|
|
1195
|
+
<TabItem label="Document Endpoints">
|
|
1196
|
+
|
|
1197
|
+
```typescript
|
|
1198
|
+
import { Controller, Get, Post, Body } from '@nestjs/common';
|
|
1199
|
+
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
|
|
1200
|
+
import { UsersService } from './users.service';
|
|
1201
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
1202
|
+
|
|
1203
|
+
@Controller('users')
|
|
1204
|
+
@ApiTags('users') // Group in Swagger UI
|
|
1205
|
+
export class UsersController {
|
|
1206
|
+
constructor(private readonly usersService: UsersService) {}
|
|
1207
|
+
|
|
1208
|
+
@Post()
|
|
1209
|
+
@ApiOperation({ summary: 'Create a new user' })
|
|
1210
|
+
@ApiBody({ type: CreateUserDto })
|
|
1211
|
+
@ApiResponse({ status: 201, description: 'User created successfully' })
|
|
1212
|
+
@ApiResponse({ status: 400, description: 'Invalid input' })
|
|
1213
|
+
create(@Body() createUserDto: CreateUserDto) {
|
|
1214
|
+
return this.usersService.create(createUserDto);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
@Get()
|
|
1218
|
+
@ApiOperation({ summary: 'Get all users' })
|
|
1219
|
+
@ApiResponse({ status: 200, description: 'List of users' })
|
|
1220
|
+
findAll() {
|
|
1221
|
+
return this.usersService.findAll();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
</TabItem>
|
|
1227
|
+
</Tabs>
|
|
1228
|
+
|
|
1229
|
+
Swagger UI will be available at `http://localhost/api-docs` by default.
|
|
1230
|
+
|
|
1231
|
+
### Authentication with Gateway
|
|
1232
|
+
|
|
1233
|
+
Services receive authenticated requests from the gateway with user context in headers:
|
|
1234
|
+
|
|
1235
|
+
```typescript
|
|
1236
|
+
import { Controller, Get, Headers } from '@nestjs/common';
|
|
1237
|
+
|
|
1238
|
+
@Controller('profile')
|
|
1239
|
+
export class ProfileController {
|
|
1240
|
+
@Get()
|
|
1241
|
+
getProfile(@Headers('x-forwarded-user-id') userId: string) {
|
|
1242
|
+
// userId is extracted from JWT by gateway
|
|
1243
|
+
return { userId, message: 'User profile' };
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
The gateway sets these headers after authentication:
|
|
1249
|
+
- `x-forwarded-user-id`: User's unique identifier
|
|
1250
|
+
- `x-forwarded-user-email`: User's email
|
|
1251
|
+
- `x-forwarded-user-operations`: Comma-separated list of granted operations
|
|
1252
|
+
|
|
1253
|
+
### Authorization
|
|
1254
|
+
|
|
1255
|
+
Use operations from `@bluealba/pae-core` for authorization:
|
|
1256
|
+
|
|
1257
|
+
```typescript
|
|
1258
|
+
import { Controller, Get, Headers, ForbiddenException } from '@nestjs/common';
|
|
1259
|
+
|
|
1260
|
+
@Controller('users')
|
|
1261
|
+
export class UsersController {
|
|
1262
|
+
@Get('admin')
|
|
1263
|
+
getAdminData(@Headers('x-forwarded-user-operations') operations: string) {
|
|
1264
|
+
const userOps = operations?.split(',') || [];
|
|
1265
|
+
|
|
1266
|
+
if (!userOps.includes('users::admin::read')) {
|
|
1267
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return { data: 'Admin data' };
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
---
|
|
1276
|
+
|
|
1277
|
+
## Catalog Registration
|
|
1278
|
+
|
|
1279
|
+
Services must register with the application catalog to be discovered by the gateway.
|
|
1280
|
+
|
|
1281
|
+
<Tabs>
|
|
1282
|
+
<TabItem label="Via API">
|
|
1283
|
+
|
|
1284
|
+
```bash
|
|
1285
|
+
POST /api/catalog
|
|
1286
|
+
Content-Type: application/json
|
|
1287
|
+
Authorization: Bearer <admin-jwt>
|
|
1288
|
+
|
|
1289
|
+
{
|
|
1290
|
+
"name": "@myorg/my-service",
|
|
1291
|
+
"type": "service",
|
|
1292
|
+
"displayName": "My Service",
|
|
1293
|
+
"baseUrl": "/api/my-service",
|
|
1294
|
+
"host": "my-service",
|
|
1295
|
+
"port": 80,
|
|
1296
|
+
"authorization": {
|
|
1297
|
+
"operations": [
|
|
1298
|
+
"my-service::read",
|
|
1299
|
+
"my-service::write"
|
|
1300
|
+
]
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
</TabItem>
|
|
1306
|
+
|
|
1307
|
+
<TabItem label="Via Bootstrap">
|
|
1308
|
+
|
|
1309
|
+
For automated deployments, include in bootstrap configuration:
|
|
1310
|
+
|
|
1311
|
+
```json
|
|
1312
|
+
{
|
|
1313
|
+
{
|
|
1314
|
+
"name": "@myorg/my-service",
|
|
1315
|
+
"displayName": "My Service",
|
|
1316
|
+
"description": "My custom service",
|
|
1317
|
+
"type": "service",
|
|
1318
|
+
"baseUrl": "/api/my-service",
|
|
1319
|
+
"service": {
|
|
1320
|
+
"host": "my-service",
|
|
1321
|
+
"port": 80
|
|
1322
|
+
},
|
|
1323
|
+
"authorization": {
|
|
1324
|
+
"routes": [
|
|
1325
|
+
{
|
|
1326
|
+
pattern: '/api/v1/users',
|
|
1327
|
+
operations: ["my-service::read"],
|
|
1328
|
+
methods: ['GET'],
|
|
1329
|
+
}
|
|
1330
|
+
]
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
</TabItem>
|
|
1337
|
+
|
|
1338
|
+
<TabItem label="Configuration Fields">
|
|
1339
|
+
|
|
1340
|
+
| Field | Description | Required |
|
|
1341
|
+
|-------|-------------|----------|
|
|
1342
|
+
| `name` | Unique service identifier (use @scope/name format) | Yes |
|
|
1343
|
+
| `displayName` | Human-readable service name | Yes |
|
|
1344
|
+
| `type` | Always `"service"` for backend services | Yes |
|
|
1345
|
+
| `baseUrl` | API path prefix (e.g., `/api/my-service`) | Yes |
|
|
1346
|
+
| `host` | Docker service name or hostname | Yes |
|
|
1347
|
+
| `port` | Service port (usually 80 in Docker) | Yes |
|
|
1348
|
+
| `authorization.operations` | List of operations required by this service | No |
|
|
1349
|
+
| `authorization.routes` | List of operations that this service requires for each of the endpoints | No |
|
|
1350
|
+
|
|
1351
|
+
</TabItem>
|
|
1352
|
+
</Tabs>
|
|
1353
|
+
|
|
1354
|
+
---
|
|
1355
|
+
|
|
1356
|
+
## Best Practices
|
|
1357
|
+
|
|
1358
|
+
### Code Organization
|
|
1359
|
+
|
|
1360
|
+
<CardGrid>
|
|
1361
|
+
<Card title="Feature-Based Modules" icon="folder">
|
|
1362
|
+
Organize code by business domain (users, products, orders) rather than technical layers.
|
|
1363
|
+
</Card>
|
|
1364
|
+
|
|
1365
|
+
<Card title="Single Responsibility" icon="document">
|
|
1366
|
+
Each service should handle one concern. Keep controllers thin and services focused.
|
|
1367
|
+
</Card>
|
|
1368
|
+
|
|
1369
|
+
<Card title="Dependency Injection" icon="puzzle">
|
|
1370
|
+
Use constructor injection for all dependencies. Avoid service locator pattern.
|
|
1371
|
+
</Card>
|
|
1372
|
+
|
|
1373
|
+
<Card title="Error Handling" icon="warning">
|
|
1374
|
+
Use NestJS built-in exceptions. Create custom exceptions for domain errors.
|
|
1375
|
+
</Card>
|
|
1376
|
+
</CardGrid>
|
|
1377
|
+
|
|
1378
|
+
**Recommended Module Structure:**
|
|
1379
|
+
|
|
1380
|
+
```
|
|
1381
|
+
src/
|
|
1382
|
+
├── users/
|
|
1383
|
+
│ ├── users.module.ts
|
|
1384
|
+
│ ├── users.controller.ts
|
|
1385
|
+
│ ├── users.service.ts
|
|
1386
|
+
│ ├── dto/
|
|
1387
|
+
│ │ ├── create-user.dto.ts
|
|
1388
|
+
│ │ └── update-user.dto.ts
|
|
1389
|
+
│ ├── entities/
|
|
1390
|
+
│ │ └── user.entity.ts
|
|
1391
|
+
│ └── tests/
|
|
1392
|
+
│ ├── users.controller.spec.ts
|
|
1393
|
+
│ └── users.service.spec.ts
|
|
1394
|
+
└── products/
|
|
1395
|
+
├── products.module.ts
|
|
1396
|
+
├── products.controller.ts
|
|
1397
|
+
├── products.service.ts
|
|
1398
|
+
└── ...
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
### Error Handling
|
|
1402
|
+
|
|
1403
|
+
```typescript
|
|
1404
|
+
import {
|
|
1405
|
+
BadRequestException,
|
|
1406
|
+
NotFoundException,
|
|
1407
|
+
ConflictException,
|
|
1408
|
+
InternalServerErrorException
|
|
1409
|
+
} from '@nestjs/common';
|
|
1410
|
+
|
|
1411
|
+
@Injectable()
|
|
1412
|
+
export class UsersService {
|
|
1413
|
+
async findOne(id: number) {
|
|
1414
|
+
const user = await this.db.findById(id);
|
|
1415
|
+
|
|
1416
|
+
if (!user) {
|
|
1417
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return user;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async create(dto: CreateUserDto) {
|
|
1424
|
+
const existing = await this.db.findByEmail(dto.email);
|
|
1425
|
+
|
|
1426
|
+
if (existing) {
|
|
1427
|
+
throw new ConflictException(`User with email ${dto.email} already exists`);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
try {
|
|
1431
|
+
return await this.db.create(dto);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
throw new InternalServerErrorException('Failed to create user');
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
### Logging
|
|
1440
|
+
|
|
1441
|
+
```typescript
|
|
1442
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
1443
|
+
|
|
1444
|
+
@Injectable()
|
|
1445
|
+
export class UsersService {
|
|
1446
|
+
private readonly logger = new Logger(UsersService.name);
|
|
1447
|
+
|
|
1448
|
+
async create(dto: CreateUserDto) {
|
|
1449
|
+
this.logger.log(`Creating user: ${dto.username}`);
|
|
1450
|
+
|
|
1451
|
+
try {
|
|
1452
|
+
const user = await this.db.create(dto);
|
|
1453
|
+
this.logger.log(`User created successfully: ${user.id}`);
|
|
1454
|
+
return user;
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
this.logger.error(`Failed to create user: ${error.message}`, error.stack);
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
### Testing
|
|
1464
|
+
|
|
1465
|
+
<Tabs>
|
|
1466
|
+
<TabItem label="Unit Test">
|
|
1467
|
+
|
|
1468
|
+
```typescript
|
|
1469
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
1470
|
+
import { UsersService } from './users.service';
|
|
1471
|
+
import { DatabaseService } from '../database/database.service';
|
|
1472
|
+
|
|
1473
|
+
describe('UsersService', () => {
|
|
1474
|
+
let service: UsersService;
|
|
1475
|
+
let dbService: DatabaseService;
|
|
1476
|
+
|
|
1477
|
+
beforeEach(async () => {
|
|
1478
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
1479
|
+
providers: [
|
|
1480
|
+
UsersService,
|
|
1481
|
+
{
|
|
1482
|
+
provide: DatabaseService,
|
|
1483
|
+
useValue: {
|
|
1484
|
+
db: jest.fn(() => ({
|
|
1485
|
+
select: jest.fn().mockReturnThis(),
|
|
1486
|
+
where: jest.fn().mockResolvedValue([{ id: 1, username: 'test' }]),
|
|
1487
|
+
})),
|
|
1488
|
+
},
|
|
1489
|
+
},
|
|
1490
|
+
],
|
|
1491
|
+
}).compile();
|
|
1492
|
+
|
|
1493
|
+
service = module.get<UsersService>(UsersService);
|
|
1494
|
+
dbService = module.get<DatabaseService>(DatabaseService);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
it('should be defined', () => {
|
|
1498
|
+
expect(service).toBeDefined();
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('should find a user by id', async () => {
|
|
1502
|
+
const user = await service.findOne(1);
|
|
1503
|
+
expect(user).toEqual({ id: 1, username: 'test' });
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
</TabItem>
|
|
1509
|
+
|
|
1510
|
+
<TabItem label="Controller Test">
|
|
1511
|
+
|
|
1512
|
+
```typescript
|
|
1513
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
1514
|
+
import { UsersController } from './users.controller';
|
|
1515
|
+
import { UsersService } from './users.service';
|
|
1516
|
+
|
|
1517
|
+
describe('UsersController', () => {
|
|
1518
|
+
let controller: UsersController;
|
|
1519
|
+
let service: UsersService;
|
|
1520
|
+
|
|
1521
|
+
beforeEach(async () => {
|
|
1522
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
1523
|
+
controllers: [UsersController],
|
|
1524
|
+
providers: [
|
|
1525
|
+
{
|
|
1526
|
+
provide: UsersService,
|
|
1527
|
+
useValue: {
|
|
1528
|
+
findAll: jest.fn().mockResolvedValue([{ id: 1, username: 'test' }]),
|
|
1529
|
+
findOne: jest.fn().mockResolvedValue({ id: 1, username: 'test' }),
|
|
1530
|
+
create: jest.fn().mockResolvedValue({ id: 1, username: 'test' }),
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
],
|
|
1534
|
+
}).compile();
|
|
1535
|
+
|
|
1536
|
+
controller = module.get<UsersController>(UsersController);
|
|
1537
|
+
service = module.get<UsersService>(UsersService);
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it('should return an array of users', async () => {
|
|
1541
|
+
const result = await controller.findAll();
|
|
1542
|
+
expect(result).toEqual([{ id: 1, username: 'test' }]);
|
|
1543
|
+
expect(service.findAll).toHaveBeenCalled();
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
</TabItem>
|
|
1549
|
+
|
|
1550
|
+
<TabItem label="E2E Test">
|
|
1551
|
+
|
|
1552
|
+
```typescript
|
|
1553
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
1554
|
+
import { INestApplication } from '@nestjs/common';
|
|
1555
|
+
import * as request from 'supertest';
|
|
1556
|
+
import { AppModule } from './../src/app.module';
|
|
1557
|
+
|
|
1558
|
+
describe('UsersController (e2e)', () => {
|
|
1559
|
+
let app: INestApplication;
|
|
1560
|
+
|
|
1561
|
+
beforeEach(async () => {
|
|
1562
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
1563
|
+
imports: [AppModule],
|
|
1564
|
+
}).compile();
|
|
1565
|
+
|
|
1566
|
+
app = moduleFixture.createNestApplication();
|
|
1567
|
+
await app.init();
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
afterEach(async () => {
|
|
1571
|
+
await app.close();
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
it('/users (GET)', () => {
|
|
1575
|
+
return request(app.getHttpServer())
|
|
1576
|
+
.get('/users')
|
|
1577
|
+
.expect(200)
|
|
1578
|
+
.expect((res) => {
|
|
1579
|
+
expect(Array.isArray(res.body)).toBe(true);
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it('/users (POST)', () => {
|
|
1584
|
+
return request(app.getHttpServer())
|
|
1585
|
+
.post('/users')
|
|
1586
|
+
.send({ username: 'test', email: 'test@example.com' })
|
|
1587
|
+
.expect(201)
|
|
1588
|
+
.expect((res) => {
|
|
1589
|
+
expect(res.body).toHaveProperty('id');
|
|
1590
|
+
expect(res.body.username).toBe('test');
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1596
|
+
</TabItem>
|
|
1597
|
+
|
|
1598
|
+
</Tabs>
|
|
1599
|
+
|
|
1600
|
+
---
|
|
1601
|
+
|
|
1602
|
+
## Troubleshooting
|
|
1603
|
+
|
|
1604
|
+
<Tabs>
|
|
1605
|
+
<TabItem label="Build Failures">
|
|
1606
|
+
|
|
1607
|
+
**Problem: Module not found errors**
|
|
1608
|
+
|
|
1609
|
+
```
|
|
1610
|
+
Error: Cannot find module '@bluealba/pae-service-nestjs-sdk'
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
**Solution:**
|
|
1614
|
+
```bash
|
|
1615
|
+
# Install missing dependencies
|
|
1616
|
+
npm install
|
|
1617
|
+
|
|
1618
|
+
# Clear node_modules and reinstall
|
|
1619
|
+
rm -rf node_modules package-lock.json
|
|
1620
|
+
npm install
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
---
|
|
1624
|
+
|
|
1625
|
+
**Problem: TypeScript compilation errors**
|
|
1626
|
+
|
|
1627
|
+
```
|
|
1628
|
+
error TS2304: Cannot find name 'Express'
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
**Solution:**
|
|
1632
|
+
```bash
|
|
1633
|
+
# Install type definitions
|
|
1634
|
+
npm install --save-dev @types/node @types/express
|
|
1635
|
+
|
|
1636
|
+
# Rebuild
|
|
1637
|
+
npm run build
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
</TabItem>
|
|
1641
|
+
|
|
1642
|
+
<TabItem label="Runtime Errors">
|
|
1643
|
+
|
|
1644
|
+
**Problem: Service doesn't start**
|
|
1645
|
+
|
|
1646
|
+
```
|
|
1647
|
+
Error: listen EADDRINUSE: address already in use :::80
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
**Solution:**
|
|
1651
|
+
```bash
|
|
1652
|
+
# Check what's using port 80
|
|
1653
|
+
lsof -i :80
|
|
1654
|
+
|
|
1655
|
+
# Kill the process or change port
|
|
1656
|
+
PORT=3000 npm run start:dev
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
---
|
|
1660
|
+
|
|
1661
|
+
**Problem: Database connection fails**
|
|
1662
|
+
|
|
1663
|
+
```
|
|
1664
|
+
Error: connect ECONNREFUSED 127.0.0.1:5432
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
**Solution:**
|
|
1668
|
+
1. Verify database is running:
|
|
1669
|
+
```bash
|
|
1670
|
+
docker-compose ps postgres
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
2. Check environment variables:
|
|
1674
|
+
```bash
|
|
1675
|
+
echo $DATABASE_URL
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
3. Test connection manually:
|
|
1679
|
+
```bash
|
|
1680
|
+
psql -h localhost -U user -d mydb
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
</TabItem>
|
|
1684
|
+
|
|
1685
|
+
<TabItem label="Docker Issues">
|
|
1686
|
+
|
|
1687
|
+
**Problem: Container exits immediately**
|
|
1688
|
+
|
|
1689
|
+
```bash
|
|
1690
|
+
# View logs
|
|
1691
|
+
docker-compose logs my-service
|
|
1692
|
+
|
|
1693
|
+
# Common causes:
|
|
1694
|
+
# - npm install failed
|
|
1695
|
+
# - Missing environment variables
|
|
1696
|
+
# - Syntax error in code
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
**Solution:**
|
|
1700
|
+
```bash
|
|
1701
|
+
# Rebuild with no cache
|
|
1702
|
+
docker-compose build --no-cache my-service
|
|
1703
|
+
|
|
1704
|
+
# Run interactively to debug
|
|
1705
|
+
docker-compose run my-service sh
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
---
|
|
1709
|
+
|
|
1710
|
+
**Problem: Code changes not reflected**
|
|
1711
|
+
|
|
1712
|
+
**Solution:**
|
|
1713
|
+
```bash
|
|
1714
|
+
# Ensure volume mount is correct in docker-compose.yml
|
|
1715
|
+
volumes:
|
|
1716
|
+
- ../my-service:/app/out/apps/my-service
|
|
1717
|
+
|
|
1718
|
+
# Restart with rebuild
|
|
1719
|
+
docker-compose up --build my-service
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
</TabItem>
|
|
1723
|
+
|
|
1724
|
+
</Tabs>
|
|
1725
|
+
|
|
1726
|
+
---
|
|
1727
|
+
|
|
1728
|
+
## Related Documentation
|
|
1729
|
+
|
|
1730
|
+
- **[Architecture Overview](../architecture/overview)** - Understand platform architecture
|
|
1731
|
+
- **[Gateway Architecture](../architecture/gateway-architecture)** - Learn about the API gateway
|
|
1732
|
+
- **[Authentication System](../architecture/authentication-system)** - Understand authentication flows
|
|
1733
|
+
- **[Authorization System](../architecture/authorization-system)** - Master operations and RBAC
|
|
1734
|
+
- **[Multi-Tenancy](../architecture/multi-tenancy)** - Learn about tenant isolation
|
|
1735
|
+
- **[Creating UI Modules](./creating-ui-modules)** - Build frontend applications
|
|
1736
|
+
- **[Using Feature Flags](./using-feature-flags)** - Implement feature toggles
|