Skip to content

Commit 027a592

Browse files
authored
Merge pull request #9 from rhyek/orval-example
include example of orval client gen
2 parents e58fa0d + f4cd635 commit 027a592

File tree

37 files changed

+1990
-67
lines changed

37 files changed

+1990
-67
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
node_modules/
22
openapi.json
3+
*.e2e-spec.ts
4+
!packages/test-endpoints-module/test-app-express-cjs/**/*.e2e-spec.ts
5+
generated/
6+
!packages/test-endpoints-module/test-app-express-cjs/generated

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ To call this endpoint:
294294
## OpenAPI, Codegen setup (optional)
295295

296296
It's a common practice to automatically generate a client SDK for your API that
297-
you can use in other backend or frontend projects and have the benefit of full-stack type-safety. tRPC and similar libraries make this easy for you.
297+
you can use in other backend or frontend projects and have the benefit of full-stack type-safety. tRPC and similar libraries have been written to facilitate this.
298298

299299
We can achieve the same here in two steps. We first build an OpenAPI document, then use that document's
300300
output with [orval](https://orval.dev/):
@@ -316,3 +316,11 @@ async function bootstrap() {
316316
await app.listen(3000);
317317
}
318318
```
319+
320+
And then you could have something like this available:
321+
322+
```typescript
323+
const { id } = await userCreate({ name: 'Tom', email: '[email protected]' });
324+
```
325+
326+
Have a look at [this](https://github.com/rhyek/nestjs-endpoints/tree/main/packages/test-endpoints-module/test-app-express-cjs) test project to see how you might configure orval to generate an axios-based client and [here](https://github.com/rhyek/nestjs-endpoints/tree/main/packages/test-endpoints-module/test-app-express-cjs/test/client.e2e-spec.ts) to understand how you would use it.

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export default tseslint.config(
7272
'@typescript-eslint/no-unsafe-argument': 'off',
7373
'@typescript-eslint/no-explicit-any': 'off',
7474
'@typescript-eslint/no-unsafe-return': 'off',
75+
'@typescript-eslint/no-redundant-type-constituents': 'off',
7576
},
7677
},
7778
{

packages/test-endpoints-module/test-app-express-cjs/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ lerna-debug.log*
3232
!.vscode/settings.json
3333
!.vscode/tasks.json
3434
!.vscode/launch.json
35-
!.vscode/extensions.json
35+
!.vscode/extensions.json
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Generated by orval v7.5.0 🍺
3+
* Do not edit manually.
4+
* OpenAPI spec version: 1.0.0
5+
*/
6+
import { customInstance } from './custom-axios-instance';
7+
export interface UserCreateInput {
8+
name: string;
9+
email: string;
10+
}
11+
12+
export interface UserCreateOutput {
13+
id: number;
14+
}
15+
16+
/**
17+
* @nullable
18+
*/
19+
export type UserFindOutput = {
20+
id: number;
21+
name: string;
22+
email: string;
23+
} | null;
24+
25+
export interface UserAppointmentCreateInput {
26+
userId: number;
27+
date: unknown;
28+
}
29+
30+
export interface UserAppointmentCreate201Output {
31+
id: number;
32+
date: unknown;
33+
address: string;
34+
}
35+
36+
export type UserAppointmentCreate400OutputOneOf = {
37+
message: string;
38+
errorCode: string;
39+
};
40+
41+
export type UserAppointmentCreate400Output =
42+
| string
43+
| UserAppointmentCreate400OutputOneOf;
44+
45+
export type UserAppointmentCountOutput = number;
46+
47+
export type UserFindParams = {
48+
id: number;
49+
};
50+
51+
export type UserAppointmentCountParams = {
52+
userId: number;
53+
};
54+
55+
type SecondParameter<T extends (...args: any) => any> = Parameters<T>[1];
56+
57+
export const userCreate = (
58+
userCreateInput: UserCreateInput,
59+
options?: SecondParameter<typeof customInstance>,
60+
) => {
61+
return customInstance<UserCreateOutput>(
62+
{
63+
url: `/user/create`,
64+
method: 'POST',
65+
headers: { 'Content-Type': 'application/json' },
66+
data: userCreateInput,
67+
},
68+
options,
69+
);
70+
};
71+
72+
export const userFind = (
73+
params: UserFindParams,
74+
options?: SecondParameter<typeof customInstance>,
75+
) => {
76+
return customInstance<UserFindOutput>(
77+
{ url: `/user/find`, method: 'GET', params },
78+
options,
79+
);
80+
};
81+
82+
/**
83+
* @summary Create an appointment
84+
*/
85+
export const userAppointmentCreate = (
86+
userAppointmentCreateInput: UserAppointmentCreateInput,
87+
options?: SecondParameter<typeof customInstance>,
88+
) => {
89+
return customInstance<UserAppointmentCreate201Output>(
90+
{
91+
url: `/user/appointment/create`,
92+
method: 'POST',
93+
headers: { 'Content-Type': 'application/json' },
94+
data: userAppointmentCreateInput,
95+
},
96+
options,
97+
);
98+
};
99+
100+
export const userAppointmentCount = (
101+
params: UserAppointmentCountParams,
102+
options?: SecondParameter<typeof customInstance>,
103+
) => {
104+
return customInstance<UserAppointmentCountOutput>(
105+
{ url: `/user/appointment/count`, method: 'GET', params },
106+
options,
107+
);
108+
};
109+
110+
export const testError = (
111+
options?: SecondParameter<typeof customInstance>,
112+
) => {
113+
return customInstance<void>(
114+
{ url: `/test/error`, method: 'GET' },
115+
options,
116+
);
117+
};
118+
119+
export const testStatus = (
120+
options?: SecondParameter<typeof customInstance>,
121+
) => {
122+
return customInstance<void>(
123+
{ url: `/test/status`, method: 'GET' },
124+
options,
125+
);
126+
};
127+
128+
export type UserCreateResult = NonNullable<
129+
Awaited<ReturnType<typeof userCreate>>
130+
>;
131+
export type UserFindResult = NonNullable<
132+
Awaited<ReturnType<typeof userFind>>
133+
>;
134+
export type UserAppointmentCreateResult = NonNullable<
135+
Awaited<ReturnType<typeof userAppointmentCreate>>
136+
>;
137+
export type UserAppointmentCountResult = NonNullable<
138+
Awaited<ReturnType<typeof userAppointmentCount>>
139+
>;
140+
export type TestErrorResult = NonNullable<
141+
Awaited<ReturnType<typeof testError>>
142+
>;
143+
export type TestStatusResult = NonNullable<
144+
Awaited<ReturnType<typeof testStatus>>
145+
>;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type AxiosRequestConfig, type AxiosInstance } from 'axios';
2+
3+
let AXIOS_INSTANCE: AxiosInstance | null = null;
4+
5+
export const setAxiosInstance = (instance: AxiosInstance) => {
6+
AXIOS_INSTANCE = instance;
7+
};
8+
9+
export const customInstance = <T>(
10+
config: AxiosRequestConfig,
11+
options?: AxiosRequestConfig,
12+
): Promise<T> => {
13+
if (!AXIOS_INSTANCE) {
14+
throw new Error('Axios instance is not initialized');
15+
}
16+
const promise = AXIOS_INSTANCE({
17+
...config,
18+
...options,
19+
}).then(({ data }) => data);
20+
return promise;
21+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineConfig } from 'orval';
2+
3+
export default defineConfig({
4+
test: {
5+
output: {
6+
workspace: 'generated',
7+
target: 'client.ts',
8+
client: 'axios-functions',
9+
mode: 'single',
10+
indexFiles: false,
11+
override: {
12+
mutator: {
13+
path: './custom-axios-instance.ts',
14+
name: 'customInstance',
15+
},
16+
},
17+
},
18+
input: {
19+
target: './openapi.json',
20+
},
21+
hooks: {
22+
afterAllFilesWrite: 'pnpm eslint --fix',
23+
},
24+
},
25+
});

packages/test-endpoints-module/test-app-express-cjs/package.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,10 @@
66
"private": true,
77
"license": "UNLICENSED",
88
"scripts": {
9-
"build": "nest build",
109
"dev": "tsx watch --include ./node_modules/nestjs-endpoints/dist --clear-screen=false --inspect=0 ./src/main.ts",
11-
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12-
"start": "nest start",
13-
"start:dev": "nest start --watch",
14-
"start:debug": "nest start --debug --watch",
10+
"build": "nest build",
1511
"start:prod": "node dist/main",
16-
"test": "jest",
17-
"test:watch": "jest --watch",
18-
"test:cov": "jest --coverage",
19-
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
12+
"codegen": "tsx ./src/codegen.ts",
2013
"test:e2e": "jest --config ./test/jest-e2e.json"
2114
},
2215
"dependencies": {
@@ -35,7 +28,9 @@
3528
"@types/jest": "^29.5.2",
3629
"@types/node": "^20.3.1",
3730
"@types/supertest": "^2.0.12",
31+
"axios": "^1.7.9",
3832
"jest": "^29.5.0",
33+
"orval": "^7.5.0",
3934
"prettier": "^3.0.0",
4035
"source-map-support": "^0.5.21",
4136
"supertest": "^7.0.0",

packages/test-endpoints-module/test-app-express-cjs/src/app.module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
} from 'nestjs-endpoints';
1313
import { Observable, catchError, throwError } from 'rxjs';
1414
import { ZodError } from 'zod';
15-
import { TestModule } from './test/test.module';
16-
import { UserModule } from './user/user.module';
15+
import { TestModule } from './endpoints/test/test.module';
16+
import { UserModule } from './endpoints/user/user.module';
1717

1818
@Injectable()
1919
export class ZodErrorInterceptor implements NestInterceptor {
@@ -35,7 +35,7 @@ export class ZodErrorInterceptor implements NestInterceptor {
3535
@Module({
3636
imports: [
3737
EndpointsRouterModule.forRoot({
38-
rootDirectory: './',
38+
rootDirectory: './endpoints',
3939
autoLoadEndpoints: false,
4040
}),
4141
UserModule,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NestFactory } from '@nestjs/core';
2+
import { setupOpenAPI } from 'nestjs-endpoints';
3+
import { generate } from 'orval';
4+
import { AppModule } from './app.module';
5+
6+
async function bootstrap() {
7+
const app = await NestFactory.create(AppModule);
8+
await setupOpenAPI(app, {
9+
outputFile: 'openapi.json',
10+
});
11+
void generate();
12+
}
13+
void bootstrap();

0 commit comments

Comments
 (0)