Skip to content

Commit f9b2d95

Browse files
Merge pull request #2 from codebuilderinc/feat/location-and-errors-endpoints
2 parents 17ac9ab + e084c56 commit f9b2d95

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1446
-422
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ pids
5454

5555
# Diagnostic reports (https://nodejs.org/api/report.html)
5656
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
57+
58+
# Sensitive / secrets (override earlier negation)
59+
.vscode/settings.json
60+
google-service-account-key.json

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,61 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
9999
## License
100100

101101
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
102+
103+
## API Conventions
104+
105+
### Response Envelope
106+
107+
Endpoints using the custom `@Api({ envelope: true })` decorator option return:
108+
109+
```
110+
{ "success": true, "data": <payload> }
111+
```
112+
113+
Errors are normalized by the global `HttpExceptionFilter` into:
114+
115+
```
116+
{
117+
"success": false,
118+
"error": {
119+
"statusCode": 400,
120+
"message": "Validation failed",
121+
"details": {},
122+
"path": "/endpoint",
123+
"timestamp": "2025-01-01T00:00:00.000Z"
124+
}
125+
}
126+
```
127+
128+
### Pagination Shape
129+
130+
Paginated endpoints (with `paginatedResponseType`) return (inside the envelope when enabled):
131+
132+
```
133+
{
134+
"items": [ ... ],
135+
"pageInfo": {
136+
"hasNextPage": true,
137+
"hasPreviousPage": false,
138+
"startCursor": "0",
139+
"endCursor": "25"
140+
},
141+
"totalCount": 1234,
142+
"meta": { "company": { ... } }
143+
}
144+
```
145+
146+
Use `buildPaginatedResult({ items, skip, take, totalCount, meta })` in services.
147+
148+
### Automatic Swagger Params
149+
150+
Annotate DTO properties with `@Field({ inQuery: true })` or `@Field({ inPath: true })`. Add those DTOs to `queriesFrom` / `pathParamsFrom` in `@Api()` and Swagger params are generated automatically.
151+
152+
### Adding a New Paginated Endpoint
153+
1. Create / reuse DTOs + annotate filters & pagination args with `@Field`.
154+
2. Controller: `@Api({ paginatedResponseType: MyDto, envelope: true, queriesFrom: [PaginationArgs, FilterDto] })`.
155+
3. Service: return `buildPaginatedResult`.
156+
157+
### Error Handling
158+
Throw standard Nest `HttpException` subclasses. The filter wraps them in the envelope. Custom non-Http errors can be mapped by extending the filter if needed.
159+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "nest build",
1010
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
1111
"start": "nest start",
12-
"start:dev": "nest start --watch",
12+
"start:dev": "NODE_ENV=local nest start --watch",
1313
"start:debug": "nest start --debug --watch",
1414
"start:prod": "node dist/main",
1515
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

src/app.controller.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Controller, Get } from '@nestjs/common';
22
import { AppService } from './app.service';
3+
import { Api } from './common/decorators/api.decorator';
34

45
@Controller()
56
export class AppController {
67
constructor(private readonly appService: AppService) {}
78

89
@Get()
9-
getHello(): string {
10-
return this.appService.getHello();
10+
@Api({ summary: 'Health / hello endpoint', description: 'Simple hello world response', envelope: true })
11+
getHello() {
12+
return { message: this.appService.getHello() };
1113
}
1214
}

src/app.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { EventsModule } from './events/events.module';
1515
import { OpenTelemetryModule } from 'nestjs-otel';
1616
import { LoggerModule } from './logger/logger.module';
1717
import { RedisModule } from './common/redis/redis.module';
18+
import { NotificationsModule } from './notifications/notifications.module';
19+
import { LocationModule } from './location/location.module';
20+
import { ErrorsModule } from './errors/errors.module';
1821

1922
//import { ConfigModule } from '@nestjs/config';
2023
//import { AppResolver } from './app.resolver';
@@ -74,6 +77,9 @@ const OpenTelemetryModuleConfig = OpenTelemetryModule.forRoot({
7477
//RedisModule,
7578

7679
WssModule,
80+
NotificationsModule,
81+
LocationModule,
82+
ErrorsModule,
7783
],
7884
providers: [],
7985
})

src/auth/auth.controller.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Body, Controller, Post, HttpCode, HttpStatus, BadRequestException } from '@nestjs/common';
22
import { AuthService } from './auth.service';
33
import { GoogleAuthInput } from './dto/google-auth.input';
4-
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
5-
import { ApiPaginationQuery } from '../common/decorators/api-nested-query.decorator';
4+
import { ApiTags } from '@nestjs/swagger';
5+
import { Api } from '../common/decorators/api.decorator';
66

77
@ApiTags('Auth')
88
@Controller('auth')
@@ -13,12 +13,16 @@ export class AuthController {
1313
* Fetch new jobs from Reddit and Web3Career, store them, and send notifications
1414
*/
1515
@Post('google')
16-
@ApiOperation({
17-
summary: 'Fetch new jobs from Reddit and Web3Career',
18-
description: 'Fetches new jobs from both sources, stores them, and sends notifications.',
16+
@Api({
17+
summary: 'Google authentication',
18+
description: 'Authenticate a user using a Google ID token and optional buildType.',
19+
bodyType: GoogleAuthInput,
20+
envelope: true,
21+
responses: [
22+
{ status: 200, description: 'Authenticated successfully.' },
23+
{ status: 400, description: 'ID token missing or invalid.' },
24+
],
1925
})
20-
@ApiParam({ name: 'idToken', description: 'Google ID Token', type: String })
21-
@ApiResponse({ status: 200, description: 'Jobs fetched and notifications sent.' })
2226
@HttpCode(HttpStatus.OK)
2327
async googleAuth(@Body() googleAuthInput: GoogleAuthInput) {
2428
const { idToken, buildType } = googleAuthInput;

src/common/configs/config.helper.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,51 @@ import { ApplicationConfig, Config } from './config.interface';
55
import config from './config';
66

77
export function loadConfig() {
8-
const filePath = `.env`; //${process.env.NODE_ENV || 'local'}
8+
// Determine which env file to load. Precedence:
9+
// 1. Explicit ENV_FILE var
10+
// 2. NODE_ENV=local|development -> .env.local (if exists)
11+
// 3. Fallback to .env
12+
const explicit = process.env.ENV_FILE;
13+
const nodeEnv = (process.env.NODE_ENV || '').toLowerCase();
14+
const candidates: string[] = [];
15+
if (explicit) {
16+
candidates.push(explicit);
17+
} else if (['local', 'development', 'dev'].includes(nodeEnv)) {
18+
candidates.push('.env.local');
19+
}
20+
candidates.push('.env');
21+
22+
let pickedPath = '.env';
23+
for (const c of candidates) {
24+
if (fs.existsSync(c)) {
25+
pickedPath = c;
26+
break;
27+
}
28+
}
929

10-
// Try to read and parse the file
1130
let file = Buffer.from('');
1231
try {
13-
file = fs.readFileSync(filePath);
32+
file = fs.readFileSync(pickedPath);
1433
} catch {
15-
/* empty */
34+
// If nothing found we proceed with empty buffer relying on process.env
1635
}
1736

1837
const dotenvConfig = dotenv.parse(file);
1938

39+
// Inject parsed values into process.env if not already present so the rest of the
40+
// application (e.g. main.ts reading process.env.PORT) sees them.
41+
for (const [k, v] of Object.entries(dotenvConfig)) {
42+
if (process.env[k] === undefined) {
43+
process.env[k] = v;
44+
}
45+
}
46+
47+
if (!process.env.ENV_FILE_LOGGED) {
48+
// Single log guard to avoid spamming on hot-reload
49+
console.log(`[config] Loaded environment file: ${pickedPath}`);
50+
process.env.ENV_FILE_LOGGED = 'true';
51+
}
52+
2053
// Parse nested JSON in file and merge with process.env
2154
const parsedConfig = parseNestedJson({
2255
...dotenvConfig,

src/common/configs/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Config } from './config.interface';
22

33
const config: Config = {
44
nest: {
5-
port: 3000,
5+
port: 4000,
66
},
77
cors: {
88
enabled: true,

0 commit comments

Comments
 (0)