Skip to content

Commit

Permalink
feat(api): integrate OpenAPI client generator for frontend
Browse files Browse the repository at this point in the history
- Add OpenAPI CLI generator dependency for TypeScript-Axios library
- Implement resource entity fetching with generated client
- Update DTO types in NestJS server
- Add client library generation instructions to README
  • Loading branch information
jimmypalelil committed Jan 29, 2025
1 parent d8de089 commit fde2ae1
Show file tree
Hide file tree
Showing 19 changed files with 2,797 additions and 241 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ env.bak/
venv.bak/
# End of https://www.toptal.com/developers/gitignore/api/node,java,python,go
.idea
.run
*.key
*.pem
*.pub
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,53 @@ cd backend
npm install
npm run dev
```
### **OpenAPI/Swagger Documentation**

The backend uses NestJS's built-in Swagger module (@nestjs/swagger) to automatically generate OpenAPI documentation from TypeScript decorators.

#### **Swagger Decorators**

Decorators are used throughout the codebase to provide metadata for API documentation:

- @ApiTags() - Groups related endpoints together
- @ApiOperation() - Describes what an endpoint does
- @ApiResponse() - Documents possible response types
- @ApiProperty() - Documents DTO properties and their types

Example usage in a controller:

```tsx
@ApiTags('parks')
@Controller('parks')
export class ParksController {
@Get()
@ApiOperation({ summary: 'Get all parks' })
@ApiResponse({
status: 200,
description: 'List of parks returned',
type: [ParkDto]
})
findAll(): Promise<ParkDto[]> {
return this.parksService.findAll();
}
}
```

#### **Accessing Generated Documentation**

When running the backend server, Swagger UI is available at:

`http://localhost:3000/api/docs`

The raw OpenAPI specification can be accessed at:

`http://localhost:3000/api/docs-json`

This documentation is automatically generated from the TypeScript code and is used to:

- Provide interactive API documentation through Swagger UI
- Generate TypeScript client types using openapi-generator
- Ensure API consistency and type safety across the frontend and backend

### Frontend

Expand All @@ -75,6 +122,30 @@ npm run dev

Navigate to `http://localhost:3000` in your web browser to view the application.

### Generate Client Library

#### Prerequisites

Install Java Development Kit (JDK) 17:

```bash
brew install openjdk@17
```

#### Generate TypeScript Axios Client

Run the following command to generate the TypeScript client library from your OpenAPI specification:

```bash
npx openapi-generator-cli generate -i http://localhost:3000/api/docs-json -g typescript-axios -o src/service/recreation-resource --skip-validate-spec
```

This command will:

- Generate TypeScript client code using Axios
- Use the OpenAPI spec from your local NestJS server which should be running on port **3000**
- Output the generated code to `src/service/recreation-resource` directory

## Pre-commit hooks

Pre-commit is set up to run checks for linting, formatting, and secrets.
Expand Down
1 change: 0 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from "@nestjs/swagger";
import { RecreationResourceDto } from "./recreation-resource.dto";

export class PaginatedRecreationResourceDto {
@ApiProperty({ type: [RecreationResourceDto] })
data: RecreationResourceDto[];

@ApiProperty()
total: number;

@ApiProperty()
page: number;
}
67 changes: 52 additions & 15 deletions backend/src/recreation-resource/dto/recreation-resource.dto.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
import { ApiProperty } from "@nestjs/swagger";

export class RecreationActivityDto {
@ApiProperty({
description: "Unique code identifying the recreation activity",
example: "HIKING",
})
recreation_activity_code: string;

@ApiProperty({
description: "Detailed description of the activity",
example: "Hiking trails available for all skill levels",
})
description: string;
}

export class RecreationStatusDto {
@ApiProperty({
description: "Status code of the resource",
})
status_code: string;

@ApiProperty({
description: "Additional status information",
example: "Temporary closure due to weather conditions",
nullable: true,
})
comment: string;

@ApiProperty({
description: "Detailed status description",
example: "The facility is currently closed to visitors",
})
description: string;
}

export class RecreationResourceDto {
@ApiProperty({
description: "The ID of the Recreation Resource",
description: "Unique identifier of the Recreation Resource",
example: "rec-123-abc",
format: "uuid",
})
rec_resource_id: string;

@ApiProperty({
description: "The name of the Recreation Resource",
description: "Official name of the Recreation Resource",
example: "Evergreen Valley Campground",
minLength: 1,
maxLength: 100,
})
name: string;

@ApiProperty({
description: "The description of the Recreation Resource",
description: "Detailed description of the Recreation Resource",
example: "A scenic campground nestled in the heart of Evergreen Valley",
nullable: true,
})
description: string;

@ApiProperty({
description: "The location of the Recreation Resource",
description: "Physical location of the Recreation Resource",
example: "123 Forest Road, Mountain View, CA 94043",
})
site_location: string;

@ApiProperty({
description: "The list of available activities at the Recreation Resource",
description: "List of recreational activities available at this resource",
type: [RecreationActivityDto],
})
recreation_activity: {
recreation_activity_code: string;
description: string;
}[];
recreation_activity: RecreationActivityDto[];

@ApiProperty({
description: "The status of the Recreation Resource",
description: "Current operational status of the Recreation Resource",
type: RecreationStatusDto,
})
recreation_status: {
status_code: string;
comment: string;
description: string;
};
recreation_status: RecreationStatusDto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,43 @@ describe("RecreationResourceController", () => {
}
});
});

describe("searchRecreationResources", () => {
it("should return paginated recreation resources", async () => {
const mockResult = {
data: [
{
rec_resource_id: "REC0001",
name: "Rec site 1",
},
],
total: 1,
page: 1,
limit: 10,
};

vi.spyOn(recService, "searchRecreationResources").mockResolvedValue(
mockResult,
);

const result = await controller.searchRecreationResources("test", 10, 1);
expect(result).toBe(mockResult);
});

it("should handle empty search results", async () => {
const mockResult = {
data: [],
total: 0,
page: 1,
limit: 10,
};

vi.spyOn(recService, "searchRecreationResources").mockResolvedValue(
mockResult,
);

const result = await controller.searchRecreationResources("", 10, 1);
expect(result).toBe(mockResult);
});
});
});
55 changes: 45 additions & 10 deletions backend/src/recreation-resource/recreation-resource.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Controller, Get, HttpException, Param, Query } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
import { RecreationResourceService } from "./recreation-resource.service";
import { RecreationResourceDto } from "./dto/recreation-resource.dto";
import { PaginatedRecreationResourceDto } from "./dto/paginated-recreation-resouce.dto";

@ApiTags("recreation-resource")
@Controller({ path: "recreation-resource", version: "1" })
Expand All @@ -10,22 +11,56 @@ export class RecreationResourceController {
private readonly recreationResourceService: RecreationResourceService,
) {}

@ApiOperation({
summary: "Search recreation resources",
operationId: "searchRecreationResources",
})
@ApiQuery({
name: "filter",
required: false,
type: String,
description: "Search filter",
})
@ApiQuery({
name: "limit",
required: false,
type: Number,
description: "Number of items per page",
})
@ApiQuery({
name: "page",
required: false,
type: Number,
description: "Page number",
})
@ApiResponse({
status: 200,
description: "Resources found",
type: PaginatedRecreationResourceDto,
})
@Get("search") // it must be ahead Get(":id") to avoid conflict
async searchRecreationResources(
@Query("filter") filter: string = "",
@Query("limit") limit?: number,
@Query("page") page: number = 1,
): Promise<{ data: RecreationResourceDto[]; total: number; page: number }> {
const response =
await this.recreationResourceService.searchRecreationResources(
page,
filter ?? "",
limit ? parseInt(String(limit)) : undefined,
);

return response;
): Promise<PaginatedRecreationResourceDto> {
return await this.recreationResourceService.searchRecreationResources(
page,
filter ?? "",
limit ? parseInt(String(limit)) : undefined,
);
}

@ApiOperation({
summary: "Find recreation resource by ID",
operationId: "getRecreationResourceById",
})
@ApiResponse({
status: 200,
description: "Resource found",
type: RecreationResourceDto,
})
@ApiResponse({ status: 404, description: "Resource not found" })
@Get(":id")
async findOne(@Param("id") id: string): Promise<RecreationResourceDto> {
const recResource = await this.recreationResourceService.findOne(id);
Expand Down
7 changes: 7 additions & 0 deletions frontend/openapitools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.11.0"
}
}
Loading

0 comments on commit fde2ae1

Please sign in to comment.