create-swagger-client 0.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.
Files changed (3) hide show
  1. package/README.md +208 -0
  2. package/dist/index.js +631 -0
  3. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # create-swagger-client
2
+
3
+ A TypeScript tool that generates a fully type-safe REST API client from OpenAPI/Swagger specifications. Built with `openapi-typescript` and `ts-morph`, it creates a strongly-typed client class with autocomplete and compile-time type checking for all API endpoints.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Full Type Safety**: All endpoints, parameters, request bodies, and responses are type-checked
8
+ - 🚀 **Auto-completion**: IDE autocomplete for paths, methods, and payloads
9
+ - 🔄 **Multiple HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH
10
+ - 📝 **OpenAPI Spec Support**: Works with OpenAPI 3.x specifications (JSON or YAML)
11
+ - 🌐 **URL or File Input**: Generate from remote URLs or local files
12
+ - 🎯 **Type Inference**: Automatic extraction of path params, query params, headers, and request/response types
13
+
14
+
15
+ ## Usage
16
+
17
+ ### Generate API Client
18
+
19
+ Run the generator with your OpenAPI specification:
20
+
21
+ ```bash
22
+ # From a URL
23
+ npx create-swagger-client https://api.example.com/openapi.json
24
+
25
+ # From a local file
26
+ npx create-swagger-client ./swagger.json
27
+
28
+ # Specify custom output file
29
+ npx create-swagger-client https://api.example.com/openapi.json my-api-client.ts
30
+ ```
31
+
32
+ **Arguments:**
33
+ - `source` (required): URL or file path to your OpenAPI/Swagger specification
34
+ - `output` (optional): Output file name (default: `client-api.ts`)
35
+
36
+ This will generate a file with:
37
+ - All TypeScript types from your OpenAPI spec
38
+ - A `RestApiClient` class with type-safe methods
39
+ - Helper types for extracting parameters and responses
40
+
41
+ ### Using the Generated Client
42
+
43
+ After generation, import and use the client:
44
+
45
+ ```typescript
46
+ import { RestApiClient } from './client-api';
47
+
48
+ // Initialize the client
49
+ const api = new RestApiClient('https://api.example.com', {
50
+ headers: {
51
+ 'Authorization': 'Bearer YOUR_TOKEN'
52
+ }
53
+ });
54
+
55
+ // Make type-safe requests
56
+ // GET request with query parameters
57
+ const users = await api.get('/users', {
58
+ query: { page: 1, limit: 10 }
59
+ });
60
+
61
+ // GET request with path parameters
62
+ const user = await api.get('/users/{id}', {
63
+ path: { id: '123' }
64
+ });
65
+
66
+ // POST request with body
67
+ const newUser = await api.post('/users', {
68
+ body: {
69
+ name: 'John Doe',
70
+ email: 'john@example.com'
71
+ }
72
+ });
73
+
74
+ // PUT request
75
+ const updatedUser = await api.put('/users/{id}', {
76
+ path: { id: '123' },
77
+ body: {
78
+ name: 'Jane Doe',
79
+ email: 'jane@example.com'
80
+ }
81
+ });
82
+
83
+ // DELETE request
84
+ await api.delete('/users/{id}', {
85
+ path: { id: '123' }
86
+ });
87
+
88
+ // PATCH request
89
+ const patchedUser = await api.patch('/users/{id}', {
90
+ path: { id: '123' },
91
+ body: {
92
+ email: 'newemail@example.com'
93
+ }
94
+ });
95
+ ```
96
+
97
+ ### Advanced Usage
98
+
99
+ #### Custom Headers per Request
100
+
101
+ ```typescript
102
+ const data = await api.get('/protected-endpoint', {
103
+ headers: {
104
+ 'X-Custom-Header': 'value'
105
+ }
106
+ });
107
+ ```
108
+
109
+ #### Request with Multiple Parameter Types
110
+
111
+ ```typescript
112
+ const result = await api.post('/projects/{projectId}/tasks', {
113
+ path: { projectId: 'proj-123' },
114
+ query: { notify: true },
115
+ headers: { 'X-Request-ID': 'req-456' },
116
+ body: {
117
+ title: 'New Task',
118
+ description: 'Task description'
119
+ }
120
+ });
121
+ ```
122
+
123
+ #### Custom Fetch Options
124
+
125
+ ```typescript
126
+ const api = new RestApiClient('https://api.example.com', {
127
+ headers: {
128
+ 'Authorization': 'Bearer TOKEN'
129
+ },
130
+ mode: 'cors',
131
+ credentials: 'include',
132
+ // Any other RequestInit options
133
+ });
134
+ ```
135
+
136
+ ## Generated Types
137
+
138
+ The generator creates several useful type utilities:
139
+
140
+ - `RestMethod`: Union of HTTP methods (`"get" | "post" | "put" | "delete" | "patch"`)
141
+ - `KeyPaths`: All available API paths
142
+ - `ExtractPathParams<Path, Method>`: Extract path parameters for an endpoint
143
+ - `ExtractQueryParams<Path, Method>`: Extract query parameters for an endpoint
144
+ - `ExtractHeaderParams<Path, Method>`: Extract header parameters for an endpoint
145
+ - `ExtractBody<Path, Method>`: Extract request body type for an endpoint
146
+ - `APIResponse<Path, Method>`: Extract response type for an endpoint
147
+ - `ApiPayload<Path, Method>`: Combined payload type for a request
148
+ - `ApiClientType`: Type definition for the entire client
149
+
150
+ ## Example: Using Generated Types
151
+
152
+ ```typescript
153
+ import {
154
+ RestApiClient,
155
+ ExtractBody,
156
+ APIResponse
157
+ } from './client-api';
158
+
159
+ // Use types in your code
160
+ type CreateUserBody = ExtractBody<'/users', 'post'>;
161
+ type UserResponse = APIResponse<'/users/{id}', 'get'>;
162
+
163
+ const createUser = (data: CreateUserBody): Promise<UserResponse> => {
164
+ return api.post('/users', { body: data });
165
+ };
166
+ ```
167
+
168
+ ## Error Handling
169
+
170
+ The client throws errors for failed requests:
171
+
172
+ ```typescript
173
+ try {
174
+ const user = await api.get('/users/{id}', {
175
+ path: { id: '123' }
176
+ });
177
+ } catch (error) {
178
+ console.error('API request failed:', error.message);
179
+ // Error message includes status code, status text, and response body
180
+ }
181
+ ```
182
+
183
+ ## Development
184
+
185
+ ### Build
186
+
187
+ ```bash
188
+ bun run build
189
+ ```
190
+
191
+ ### Type Check
192
+
193
+ ```bash
194
+ bun run typecheck
195
+ ```
196
+
197
+ ## Requirements
198
+
199
+ - TypeScript 5.x
200
+ - Node.js 16+ or Bun
201
+
202
+ ## License
203
+
204
+ MIT
205
+
206
+ ## Contributing
207
+
208
+ Contributions are welcome! Please feel free to submit a Pull Request.
package/dist/index.js ADDED
@@ -0,0 +1,631 @@
1
+ // index.ts
2
+ import openapiTS, { astToString } from "openapi-typescript";
3
+
4
+ // node:path
5
+ function assertPath(path) {
6
+ if (typeof path !== "string")
7
+ throw TypeError("Path must be a string. Received " + JSON.stringify(path));
8
+ }
9
+ function normalizeStringPosix(path, allowAboveRoot) {
10
+ var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
11
+ for (var i = 0;i <= path.length; ++i) {
12
+ if (i < path.length)
13
+ code = path.charCodeAt(i);
14
+ else if (code === 47)
15
+ break;
16
+ else
17
+ code = 47;
18
+ if (code === 47) {
19
+ if (lastSlash === i - 1 || dots === 1)
20
+ ;
21
+ else if (lastSlash !== i - 1 && dots === 2) {
22
+ if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
23
+ if (res.length > 2) {
24
+ var lastSlashIndex = res.lastIndexOf("/");
25
+ if (lastSlashIndex !== res.length - 1) {
26
+ if (lastSlashIndex === -1)
27
+ res = "", lastSegmentLength = 0;
28
+ else
29
+ res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
30
+ lastSlash = i, dots = 0;
31
+ continue;
32
+ }
33
+ } else if (res.length === 2 || res.length === 1) {
34
+ res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
35
+ continue;
36
+ }
37
+ }
38
+ if (allowAboveRoot) {
39
+ if (res.length > 0)
40
+ res += "/..";
41
+ else
42
+ res = "..";
43
+ lastSegmentLength = 2;
44
+ }
45
+ } else {
46
+ if (res.length > 0)
47
+ res += "/" + path.slice(lastSlash + 1, i);
48
+ else
49
+ res = path.slice(lastSlash + 1, i);
50
+ lastSegmentLength = i - lastSlash - 1;
51
+ }
52
+ lastSlash = i, dots = 0;
53
+ } else if (code === 46 && dots !== -1)
54
+ ++dots;
55
+ else
56
+ dots = -1;
57
+ }
58
+ return res;
59
+ }
60
+ function _format(sep, pathObject) {
61
+ var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
62
+ if (!dir)
63
+ return base;
64
+ if (dir === pathObject.root)
65
+ return dir + base;
66
+ return dir + sep + base;
67
+ }
68
+ function resolve() {
69
+ var resolvedPath = "", resolvedAbsolute = false, cwd;
70
+ for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
71
+ var path;
72
+ if (i >= 0)
73
+ path = arguments[i];
74
+ else {
75
+ if (cwd === undefined)
76
+ cwd = process.cwd();
77
+ path = cwd;
78
+ }
79
+ if (assertPath(path), path.length === 0)
80
+ continue;
81
+ resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
82
+ }
83
+ if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
84
+ if (resolvedPath.length > 0)
85
+ return "/" + resolvedPath;
86
+ else
87
+ return "/";
88
+ else if (resolvedPath.length > 0)
89
+ return resolvedPath;
90
+ else
91
+ return ".";
92
+ }
93
+ function normalize(path) {
94
+ if (assertPath(path), path.length === 0)
95
+ return ".";
96
+ var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
97
+ if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
98
+ path = ".";
99
+ if (path.length > 0 && trailingSeparator)
100
+ path += "/";
101
+ if (isAbsolute)
102
+ return "/" + path;
103
+ return path;
104
+ }
105
+ function isAbsolute(path) {
106
+ return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
107
+ }
108
+ function join() {
109
+ if (arguments.length === 0)
110
+ return ".";
111
+ var joined;
112
+ for (var i = 0;i < arguments.length; ++i) {
113
+ var arg = arguments[i];
114
+ if (assertPath(arg), arg.length > 0)
115
+ if (joined === undefined)
116
+ joined = arg;
117
+ else
118
+ joined += "/" + arg;
119
+ }
120
+ if (joined === undefined)
121
+ return ".";
122
+ return normalize(joined);
123
+ }
124
+ function relative(from, to) {
125
+ if (assertPath(from), assertPath(to), from === to)
126
+ return "";
127
+ if (from = resolve(from), to = resolve(to), from === to)
128
+ return "";
129
+ var fromStart = 1;
130
+ for (;fromStart < from.length; ++fromStart)
131
+ if (from.charCodeAt(fromStart) !== 47)
132
+ break;
133
+ var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
134
+ for (;toStart < to.length; ++toStart)
135
+ if (to.charCodeAt(toStart) !== 47)
136
+ break;
137
+ var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
138
+ for (;i <= length; ++i) {
139
+ if (i === length) {
140
+ if (toLen > length) {
141
+ if (to.charCodeAt(toStart + i) === 47)
142
+ return to.slice(toStart + i + 1);
143
+ else if (i === 0)
144
+ return to.slice(toStart + i);
145
+ } else if (fromLen > length) {
146
+ if (from.charCodeAt(fromStart + i) === 47)
147
+ lastCommonSep = i;
148
+ else if (i === 0)
149
+ lastCommonSep = 0;
150
+ }
151
+ break;
152
+ }
153
+ var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
154
+ if (fromCode !== toCode)
155
+ break;
156
+ else if (fromCode === 47)
157
+ lastCommonSep = i;
158
+ }
159
+ var out = "";
160
+ for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
161
+ if (i === fromEnd || from.charCodeAt(i) === 47)
162
+ if (out.length === 0)
163
+ out += "..";
164
+ else
165
+ out += "/..";
166
+ if (out.length > 0)
167
+ return out + to.slice(toStart + lastCommonSep);
168
+ else {
169
+ if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
170
+ ++toStart;
171
+ return to.slice(toStart);
172
+ }
173
+ }
174
+ function _makeLong(path) {
175
+ return path;
176
+ }
177
+ function dirname(path) {
178
+ if (assertPath(path), path.length === 0)
179
+ return ".";
180
+ var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
181
+ for (var i = path.length - 1;i >= 1; --i)
182
+ if (code = path.charCodeAt(i), code === 47) {
183
+ if (!matchedSlash) {
184
+ end = i;
185
+ break;
186
+ }
187
+ } else
188
+ matchedSlash = false;
189
+ if (end === -1)
190
+ return hasRoot ? "/" : ".";
191
+ if (hasRoot && end === 1)
192
+ return "//";
193
+ return path.slice(0, end);
194
+ }
195
+ function basename(path, ext) {
196
+ if (ext !== undefined && typeof ext !== "string")
197
+ throw TypeError('"ext" argument must be a string');
198
+ assertPath(path);
199
+ var start = 0, end = -1, matchedSlash = true, i;
200
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
201
+ if (ext.length === path.length && ext === path)
202
+ return "";
203
+ var extIdx = ext.length - 1, firstNonSlashEnd = -1;
204
+ for (i = path.length - 1;i >= 0; --i) {
205
+ var code = path.charCodeAt(i);
206
+ if (code === 47) {
207
+ if (!matchedSlash) {
208
+ start = i + 1;
209
+ break;
210
+ }
211
+ } else {
212
+ if (firstNonSlashEnd === -1)
213
+ matchedSlash = false, firstNonSlashEnd = i + 1;
214
+ if (extIdx >= 0)
215
+ if (code === ext.charCodeAt(extIdx)) {
216
+ if (--extIdx === -1)
217
+ end = i;
218
+ } else
219
+ extIdx = -1, end = firstNonSlashEnd;
220
+ }
221
+ }
222
+ if (start === end)
223
+ end = firstNonSlashEnd;
224
+ else if (end === -1)
225
+ end = path.length;
226
+ return path.slice(start, end);
227
+ } else {
228
+ for (i = path.length - 1;i >= 0; --i)
229
+ if (path.charCodeAt(i) === 47) {
230
+ if (!matchedSlash) {
231
+ start = i + 1;
232
+ break;
233
+ }
234
+ } else if (end === -1)
235
+ matchedSlash = false, end = i + 1;
236
+ if (end === -1)
237
+ return "";
238
+ return path.slice(start, end);
239
+ }
240
+ }
241
+ function extname(path) {
242
+ assertPath(path);
243
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
244
+ for (var i = path.length - 1;i >= 0; --i) {
245
+ var code = path.charCodeAt(i);
246
+ if (code === 47) {
247
+ if (!matchedSlash) {
248
+ startPart = i + 1;
249
+ break;
250
+ }
251
+ continue;
252
+ }
253
+ if (end === -1)
254
+ matchedSlash = false, end = i + 1;
255
+ if (code === 46) {
256
+ if (startDot === -1)
257
+ startDot = i;
258
+ else if (preDotState !== 1)
259
+ preDotState = 1;
260
+ } else if (startDot !== -1)
261
+ preDotState = -1;
262
+ }
263
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
264
+ return "";
265
+ return path.slice(startDot, end);
266
+ }
267
+ function format(pathObject) {
268
+ if (pathObject === null || typeof pathObject !== "object")
269
+ throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
270
+ return _format("/", pathObject);
271
+ }
272
+ function parse(path) {
273
+ assertPath(path);
274
+ var ret = { root: "", dir: "", base: "", ext: "", name: "" };
275
+ if (path.length === 0)
276
+ return ret;
277
+ var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
278
+ if (isAbsolute2)
279
+ ret.root = "/", start = 1;
280
+ else
281
+ start = 0;
282
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
283
+ for (;i >= start; --i) {
284
+ if (code = path.charCodeAt(i), code === 47) {
285
+ if (!matchedSlash) {
286
+ startPart = i + 1;
287
+ break;
288
+ }
289
+ continue;
290
+ }
291
+ if (end === -1)
292
+ matchedSlash = false, end = i + 1;
293
+ if (code === 46) {
294
+ if (startDot === -1)
295
+ startDot = i;
296
+ else if (preDotState !== 1)
297
+ preDotState = 1;
298
+ } else if (startDot !== -1)
299
+ preDotState = -1;
300
+ }
301
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
302
+ if (end !== -1)
303
+ if (startPart === 0 && isAbsolute2)
304
+ ret.base = ret.name = path.slice(1, end);
305
+ else
306
+ ret.base = ret.name = path.slice(startPart, end);
307
+ } else {
308
+ if (startPart === 0 && isAbsolute2)
309
+ ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
310
+ else
311
+ ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
312
+ ret.ext = path.slice(startDot, end);
313
+ }
314
+ if (startPart > 0)
315
+ ret.dir = path.slice(0, startPart - 1);
316
+ else if (isAbsolute2)
317
+ ret.dir = "/";
318
+ return ret;
319
+ }
320
+ var sep = "/";
321
+ var delimiter = ":";
322
+ var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
323
+
324
+ // index.ts
325
+ import * as tsMorph from "ts-morph";
326
+ var args = process.argv.slice(2);
327
+ var source = args[0];
328
+ var outPut = args[1] || "client-api.ts";
329
+ if (!source) {
330
+ console.error("Please provide a source URL or file path.");
331
+ process.exit(1);
332
+ }
333
+ var isUrl = (str) => {
334
+ try {
335
+ new URL(str);
336
+ return true;
337
+ } catch {
338
+ return false;
339
+ }
340
+ };
341
+ async function generate() {
342
+ if (!source)
343
+ return;
344
+ if (isUrl(source) === false) {
345
+ source = resolve(process.cwd(), source);
346
+ }
347
+ console.log(`Generating API client from ${source}...`);
348
+ const ast = await openapiTS(source);
349
+ const contents = astToString(ast);
350
+ const project = new tsMorph.Project;
351
+ const sourceFile = project.createSourceFile(resolve(process.cwd(), outPut), contents, {
352
+ overwrite: true
353
+ });
354
+ sourceFile.addTypeAlias({
355
+ name: "RestMethod",
356
+ isExported: true,
357
+ type: '"get" | "post" | "put" | "delete" | "patch"'
358
+ });
359
+ sourceFile.addTypeAlias({
360
+ name: "KeyPaths",
361
+ isExported: true,
362
+ type: "keyof paths"
363
+ });
364
+ sourceFile.addTypeAlias({
365
+ name: "ExtractPathParams",
366
+ isExported: true,
367
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
368
+ type: "paths[T][K] extends { parameters: { path?: infer P } } ? P : never"
369
+ });
370
+ sourceFile.addTypeAlias({
371
+ name: "ExtractQueryParams",
372
+ isExported: true,
373
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
374
+ type: "paths[T][K] extends { parameters: { query?: infer Q } } ? Q : never"
375
+ });
376
+ sourceFile.addTypeAlias({
377
+ name: "ExtractHeaderParams",
378
+ isExported: true,
379
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
380
+ type: "paths[T][K] extends { parameters: { header?: infer H } } ? H : never"
381
+ });
382
+ sourceFile.addTypeAlias({
383
+ name: "ExtractBody",
384
+ isExported: true,
385
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
386
+ type: `paths[T][K] extends {
387
+ requestBody: { content: { "application/json": infer B } };
388
+ }
389
+ ? B
390
+ : never`
391
+ });
392
+ sourceFile.addTypeAlias({
393
+ name: "APIResponse",
394
+ isExported: true,
395
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
396
+ type: `paths[T][K] extends {
397
+ responses:
398
+ | { content: { "application/json": infer R } }
399
+ | { [code: number]: { content: { "application/json": infer R } } };
400
+ }
401
+ ? R
402
+ : unknown`
403
+ });
404
+ sourceFile.addTypeAlias({
405
+ name: "ApiPayload",
406
+ isExported: true,
407
+ typeParameters: ["T extends KeyPaths", "K extends RestMethod"],
408
+ type: `{
409
+ path?: ExtractPathParams<T, K>;
410
+ query?: ExtractQueryParams<T, K>;
411
+ body?: K extends "post" | "put" | "patch" ? ExtractBody<T, K> : never;
412
+ headers?: ExtractHeaderParams<T, K>;
413
+ }`
414
+ });
415
+ sourceFile.addTypeAlias({
416
+ name: "ApiClientType",
417
+ isExported: true,
418
+ type: `{
419
+ [K in RestMethod]: <T extends KeyPaths>(
420
+ path: T,
421
+ payload?: ApiPayload<T, K>,
422
+ ) => Promise<APIResponse<T, K>>;
423
+ }`
424
+ });
425
+ sourceFile.addTypeAlias({
426
+ name: "TypePaths",
427
+ typeParameters: ["T extends RestMethod"],
428
+ type: `{
429
+ [K in KeyPaths]: paths[K] extends { [M in T]: unknown } ? K : never;
430
+ }[KeyPaths]`
431
+ });
432
+ sourceFile.addClass({
433
+ name: "RestApiClient",
434
+ isExported: true,
435
+ ctors: [
436
+ {
437
+ parameters: [
438
+ { name: "basePath", type: "string", scope: tsMorph.Scope.Private },
439
+ {
440
+ name: "option",
441
+ type: "RequestInit",
442
+ hasQuestionToken: true,
443
+ scope: tsMorph.Scope.Private
444
+ }
445
+ ]
446
+ }
447
+ ],
448
+ methods: [
449
+ {
450
+ name: "fetcher",
451
+ scope: tsMorph.Scope.Public,
452
+ isAsync: true,
453
+ parameters: [
454
+ { name: "input", type: "RequestInfo" },
455
+ { name: "init", type: "RequestInit", hasQuestionToken: true }
456
+ ],
457
+ statements: `const headers = {
458
+ "Content-Type": "application/json",
459
+ ...init?.headers,
460
+ };
461
+
462
+ const response = await fetch(input, { ...init, headers });
463
+ if (!response.ok) {
464
+ const errorBody = await response.text();
465
+ throw new Error(
466
+ \`API request failed: \${response.status} \${response.statusText} - \${errorBody}\`,
467
+ );
468
+ }
469
+ return response.json();`
470
+ },
471
+ {
472
+ name: "request",
473
+ typeParameters: ["M extends RestMethod", "P extends TypePaths<M>"],
474
+ parameters: [
475
+ { name: "method", type: "M" },
476
+ { name: "path", type: "P" },
477
+ {
478
+ name: "init",
479
+ type: "ApiPayload<P, M>",
480
+ initializer: "{} as ApiPayload<P, M>"
481
+ }
482
+ ],
483
+ returnType: "Promise<APIResponse<P, M>>",
484
+ statements: `const url = new URL(this.basePath + String(path));
485
+
486
+ url.pathname = this.buildPathUrl(url.pathname, init.path);
487
+ this.appendQueryParams(url, init.query);
488
+
489
+ const requestInit: RequestInit = {
490
+ method: method.toUpperCase(),
491
+ ...this.option,
492
+ headers: {
493
+ ...(this.option?.headers ?? {}),
494
+ ...(init.headers ?? {}),
495
+ },
496
+ body: this.prepareBody(method, init.body),
497
+ };
498
+
499
+ return this.fetcher(url.toString(), requestInit) as Promise<
500
+ APIResponse<P, M>
501
+ >;`
502
+ },
503
+ {
504
+ name: "get",
505
+ scope: tsMorph.Scope.Public,
506
+ typeParameters: ['T extends TypePaths<"get">'],
507
+ parameters: [
508
+ { name: "path", type: "T" },
509
+ {
510
+ name: "payload",
511
+ type: 'ApiPayload<T, "get">',
512
+ hasQuestionToken: true
513
+ }
514
+ ],
515
+ returnType: 'Promise<APIResponse<T, "get">>',
516
+ statements: 'return this.request("get", path, payload);'
517
+ },
518
+ {
519
+ name: "post",
520
+ scope: tsMorph.Scope.Public,
521
+ typeParameters: ['T extends TypePaths<"post">'],
522
+ parameters: [
523
+ { name: "path", type: "T" },
524
+ {
525
+ name: "payload",
526
+ type: 'ApiPayload<T, "post">',
527
+ hasQuestionToken: true
528
+ }
529
+ ],
530
+ returnType: 'Promise<APIResponse<T, "post">>',
531
+ statements: 'return this.request("post", path, payload);'
532
+ },
533
+ {
534
+ name: "put",
535
+ scope: tsMorph.Scope.Public,
536
+ typeParameters: ['T extends TypePaths<"put">'],
537
+ parameters: [
538
+ { name: "path", type: "T" },
539
+ {
540
+ name: "payload",
541
+ type: 'ApiPayload<T, "put">',
542
+ hasQuestionToken: true
543
+ }
544
+ ],
545
+ returnType: 'Promise<APIResponse<T, "put">>',
546
+ statements: 'return this.request("put", path, payload);'
547
+ },
548
+ {
549
+ name: "delete",
550
+ scope: tsMorph.Scope.Public,
551
+ typeParameters: ['T extends TypePaths<"delete">'],
552
+ parameters: [
553
+ { name: "path", type: "T" },
554
+ {
555
+ name: "payload",
556
+ type: 'ApiPayload<T, "delete">',
557
+ hasQuestionToken: true
558
+ }
559
+ ],
560
+ returnType: 'Promise<APIResponse<T, "delete">>',
561
+ statements: 'return this.request("delete", path, payload);'
562
+ },
563
+ {
564
+ name: "patch",
565
+ scope: tsMorph.Scope.Public,
566
+ typeParameters: ['T extends TypePaths<"patch">'],
567
+ parameters: [
568
+ { name: "path", type: "T" },
569
+ {
570
+ name: "payload",
571
+ type: 'ApiPayload<T, "patch">',
572
+ hasQuestionToken: true
573
+ }
574
+ ],
575
+ returnType: 'Promise<APIResponse<T, "patch">>',
576
+ statements: 'return this.request("patch", path, payload);'
577
+ },
578
+ {
579
+ name: "buildPathUrl",
580
+ scope: tsMorph.Scope.Private,
581
+ parameters: [
582
+ { name: "basePath", type: "string" },
583
+ { name: "pathParams", type: "unknown", hasQuestionToken: true }
584
+ ],
585
+ returnType: "string",
586
+ statements: `let pathname = basePath;
587
+ if (pathParams != null) {
588
+ const params = pathParams as Record<string, unknown>;
589
+ pathname = decodeURIComponent(pathname).replace(/{(w+)}/g, (_, key) =>
590
+ encodeURIComponent(String(params[key])),
591
+ );
592
+ }
593
+ return pathname;`
594
+ },
595
+ {
596
+ name: "prepareBody",
597
+ scope: tsMorph.Scope.Private,
598
+ parameters: [
599
+ { name: "method", type: "RestMethod" },
600
+ { name: "body", type: "unknown", hasQuestionToken: true }
601
+ ],
602
+ returnType: "string | undefined",
603
+ statements: `if (body && ["post", "put", "patch"].includes(method)) {
604
+ return JSON.stringify(body);
605
+ }
606
+ return undefined;`
607
+ },
608
+ {
609
+ name: "appendQueryParams",
610
+ scope: tsMorph.Scope.Private,
611
+ parameters: [
612
+ { name: "url", type: "URL" },
613
+ { name: "queryParams", type: "unknown", hasQuestionToken: true }
614
+ ],
615
+ returnType: "void",
616
+ statements: `if (queryParams != null) {
617
+ const params = queryParams as Record<string, unknown>;
618
+ for (const [key, value] of Object.entries(params)) {
619
+ if (value !== undefined && value !== null) {
620
+ url.searchParams.append(key, String(value));
621
+ }
622
+ }
623
+ }`
624
+ }
625
+ ]
626
+ });
627
+ await sourceFile.formatText();
628
+ await project.save();
629
+ console.log(`API client generated at ${outPut}`);
630
+ }
631
+ generate();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "create-swagger-client",
3
+ "version": "0.1.0",
4
+ "description": "Generate fully type-safe REST API clients from OpenAPI/Swagger specifications",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "type": "module",
8
+ "bin": {
9
+ "create-swagger-client": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "bun build index.ts --outdir dist --external openapi-typescript --external ts-morph",
16
+ "typecheck": "tsc --noEmit",
17
+ "prepublishOnly": "bun run build"
18
+ },
19
+ "keywords": [
20
+ "openapi",
21
+ "swagger",
22
+ "typescript",
23
+ "api-client",
24
+ "type-safe",
25
+ "rest-api",
26
+ "code-generator",
27
+ "openapi-generator",
28
+ "swagger-codegen"
29
+ ],
30
+ "author": "cuongboi",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/cuongboi/create-swagger-client.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/cuongboi/create-swagger-client/issues"
38
+ },
39
+ "homepage": "https://github.com/cuongboi/create-swagger-client#readme",
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "latest"
45
+ },
46
+ "peerDependencies": {
47
+ "typescript": "^5"
48
+ },
49
+ "dependencies": {
50
+ "openapi-typescript": "^7.10.1",
51
+ "ts-morph": "^27.0.2"
52
+ }
53
+ }