snap-on-openapi 1.0.1

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 (131) hide show
  1. package/README.md +643 -0
  2. package/dist/OpenApi.d.ts +58 -0
  3. package/dist/OpenApi.js +272 -0
  4. package/dist/README.md +643 -0
  5. package/dist/assets/stoplight.html +27 -0
  6. package/dist/assets/swagger.html +892 -0
  7. package/dist/enums/ErrorCode.d.ts +5 -0
  8. package/dist/enums/ErrorCode.js +6 -0
  9. package/dist/enums/Methods.d.ts +7 -0
  10. package/dist/enums/Methods.js +8 -0
  11. package/dist/enums/SampleRouteType.d.ts +3 -0
  12. package/dist/enums/SampleRouteType.js +4 -0
  13. package/dist/enums/ValidationLocations.d.ts +6 -0
  14. package/dist/enums/ValidationLocations.js +7 -0
  15. package/dist/index.d.ts +39 -0
  16. package/dist/index.js +23 -0
  17. package/dist/services/ClientGenerator/ClientGenerator.d.ts +9 -0
  18. package/dist/services/ClientGenerator/ClientGenerator.js +48 -0
  19. package/dist/services/ConfigBuilder/ConfigBuilder.d.ts +31 -0
  20. package/dist/services/ConfigBuilder/ConfigBuilder.js +65 -0
  21. package/dist/services/ConfigBuilder/types/DefaultConfig.d.ts +22 -0
  22. package/dist/services/ConfigBuilder/types/DefaultConfig.js +47 -0
  23. package/dist/services/ConfigBuilder/types/DefaultErrorMap.d.ts +113 -0
  24. package/dist/services/ConfigBuilder/types/DefaultErrorMap.js +21 -0
  25. package/dist/services/ConfigBuilder/types/DefaultRouteContextMap.d.ts +6 -0
  26. package/dist/services/ConfigBuilder/types/DefaultRouteContextMap.js +3 -0
  27. package/dist/services/ConfigBuilder/types/DefaultRouteMap.d.ts +12 -0
  28. package/dist/services/ConfigBuilder/types/DefaultRouteMap.js +9 -0
  29. package/dist/services/ConfigBuilder/types/DefaultRouteParamsMap.d.ts +5 -0
  30. package/dist/services/ConfigBuilder/types/DefaultRouteParamsMap.js +3 -0
  31. package/dist/services/ConfigBuilder/types/OpenApiConstructor.d.ts +2 -0
  32. package/dist/services/ConfigBuilder/types/OpenApiConstructor.js +1 -0
  33. package/dist/services/DescriptionChecker/DescriptionChecker.d.ts +16 -0
  34. package/dist/services/DescriptionChecker/DescriptionChecker.js +47 -0
  35. package/dist/services/DevelopmentUtils/DevelopmentUtils.d.ts +4 -0
  36. package/dist/services/DevelopmentUtils/DevelopmentUtils.js +15 -0
  37. package/dist/services/ExpressWrapper/ExpressWrapper.d.ts +15 -0
  38. package/dist/services/ExpressWrapper/ExpressWrapper.js +68 -0
  39. package/dist/services/ExpressWrapper/types/ExpressApp.d.ts +8 -0
  40. package/dist/services/ExpressWrapper/types/ExpressApp.js +1 -0
  41. package/dist/services/ExpressWrapper/types/ExpressHandler.d.ts +3 -0
  42. package/dist/services/ExpressWrapper/types/ExpressHandler.js +1 -0
  43. package/dist/services/ExpressWrapper/types/ExpressRequest.d.ts +8 -0
  44. package/dist/services/ExpressWrapper/types/ExpressRequest.js +1 -0
  45. package/dist/services/ExpressWrapper/types/ExpressResponse.d.ts +6 -0
  46. package/dist/services/ExpressWrapper/types/ExpressResponse.js +1 -0
  47. package/dist/services/Logger/Logger.d.ts +15 -0
  48. package/dist/services/Logger/Logger.js +100 -0
  49. package/dist/services/Logger/types/LogLevel.d.ts +6 -0
  50. package/dist/services/Logger/types/LogLevel.js +7 -0
  51. package/dist/services/RoutingFactory/RoutingFactory.d.ts +10 -0
  52. package/dist/services/RoutingFactory/RoutingFactory.js +23 -0
  53. package/dist/services/SchemaGenerator/SchemaGenerator.d.ts +19 -0
  54. package/dist/services/SchemaGenerator/SchemaGenerator.js +131 -0
  55. package/dist/services/TanstackStartWrapper/TanstackStartWrapper.d.ts +35 -0
  56. package/dist/services/TanstackStartWrapper/TanstackStartWrapper.js +67 -0
  57. package/dist/services/TestUtils/TestUtils.d.ts +13 -0
  58. package/dist/services/TestUtils/TestUtils.js +48 -0
  59. package/dist/services/ValidationUtils/ValidationUtils.d.ts +56 -0
  60. package/dist/services/ValidationUtils/ValidationUtils.js +38 -0
  61. package/dist/services/ValidationUtils/transformers/stringBooleanTransformer.d.ts +3 -0
  62. package/dist/services/ValidationUtils/transformers/stringBooleanTransformer.js +5 -0
  63. package/dist/services/ValidationUtils/transformers/stringDateTransformer.d.ts +3 -0
  64. package/dist/services/ValidationUtils/transformers/stringDateTransformer.js +16 -0
  65. package/dist/services/ValidationUtils/transformers/stringNumberTransfromer.d.ts +3 -0
  66. package/dist/services/ValidationUtils/transformers/stringNumberTransfromer.js +24 -0
  67. package/dist/services/ValidationUtils/types/PaginatedResponse.d.ts +8 -0
  68. package/dist/services/ValidationUtils/types/PaginatedResponse.js +1 -0
  69. package/dist/types/AnyRoute.d.ts +3 -0
  70. package/dist/types/AnyRoute.js +1 -0
  71. package/dist/types/InitialBuilder.d.ts +9 -0
  72. package/dist/types/InitialBuilder.js +1 -0
  73. package/dist/types/NonEmptyArray.d.ts +1 -0
  74. package/dist/types/NonEmptyArray.js +1 -0
  75. package/dist/types/Route.d.ts +22 -0
  76. package/dist/types/Route.js +1 -0
  77. package/dist/types/RouteMap.d.ts +3 -0
  78. package/dist/types/RouteMap.js +1 -0
  79. package/dist/types/RoutePath.d.ts +1 -0
  80. package/dist/types/RoutePath.js +1 -0
  81. package/dist/types/Wrappers.d.ts +7 -0
  82. package/dist/types/Wrappers.js +1 -0
  83. package/dist/types/config/AnyConfig.d.ts +4 -0
  84. package/dist/types/config/AnyConfig.js +1 -0
  85. package/dist/types/config/AnyRouteConfigMap.d.ts +4 -0
  86. package/dist/types/config/AnyRouteConfigMap.js +1 -0
  87. package/dist/types/config/Config.d.ts +19 -0
  88. package/dist/types/config/Config.js +1 -0
  89. package/dist/types/config/ContextParams.d.ts +12 -0
  90. package/dist/types/config/ContextParams.js +1 -0
  91. package/dist/types/config/ErrorConfig.d.ts +6 -0
  92. package/dist/types/config/ErrorConfig.js +1 -0
  93. package/dist/types/config/ErrorConfigMap.d.ts +5 -0
  94. package/dist/types/config/ErrorConfigMap.js +1 -0
  95. package/dist/types/config/ErrorResponse.d.ts +8 -0
  96. package/dist/types/config/ErrorResponse.js +1 -0
  97. package/dist/types/config/ISpecificationExtension.d.ts +6 -0
  98. package/dist/types/config/ISpecificationExtension.js +1 -0
  99. package/dist/types/config/Info.d.ts +7 -0
  100. package/dist/types/config/Info.js +1 -0
  101. package/dist/types/config/OmitMappedField.d.ts +3 -0
  102. package/dist/types/config/OmitMappedField.js +1 -0
  103. package/dist/types/config/RouteConfig.d.ts +10 -0
  104. package/dist/types/config/RouteConfig.js +1 -0
  105. package/dist/types/config/RouteConfigMap.d.ts +7 -0
  106. package/dist/types/config/RouteConfigMap.js +1 -0
  107. package/dist/types/config/RouteContextMap.d.ts +6 -0
  108. package/dist/types/config/RouteContextMap.js +1 -0
  109. package/dist/types/config/RouteExtraProps.d.ts +2 -0
  110. package/dist/types/config/RouteExtraProps.js +1 -0
  111. package/dist/types/config/RouteExtraPropsMap.d.ts +4 -0
  112. package/dist/types/config/RouteExtraPropsMap.js +1 -0
  113. package/dist/types/config/Server.d.ts +5 -0
  114. package/dist/types/config/Server.js +1 -0
  115. package/dist/types/errors/ApiError.d.ts +5 -0
  116. package/dist/types/errors/ApiError.js +10 -0
  117. package/dist/types/errors/BuiltInError.d.ts +4 -0
  118. package/dist/types/errors/BuiltInError.js +3 -0
  119. package/dist/types/errors/FieldError.d.ts +33 -0
  120. package/dist/types/errors/FieldError.js +9 -0
  121. package/dist/types/errors/ValidationError.d.ts +10 -0
  122. package/dist/types/errors/ValidationError.js +17 -0
  123. package/dist/types/errors/responses/NotFoundErrorResponse.d.ts +12 -0
  124. package/dist/types/errors/responses/NotFoundErrorResponse.js +6 -0
  125. package/dist/types/errors/responses/UnknownErrorResponse.d.ts +12 -0
  126. package/dist/types/errors/responses/UnknownErrorResponse.js +6 -0
  127. package/dist/types/errors/responses/ValidationErrorResponse.d.ts +89 -0
  128. package/dist/types/errors/responses/ValidationErrorResponse.js +12 -0
  129. package/dist/vitest.config.d.ts +2 -0
  130. package/dist/vitest.config.js +25 -0
  131. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,643 @@
1
+ # Strap-on OpenAPI
2
+
3
+ Bring a fully-fledged, type-checked API to your app in just 5 minutes.
4
+
5
+ [What is Strap-On OpenAPI](#what-is-strap-on-openapi)
6
+
7
+ [Installation](#installation)
8
+
9
+ [Quick Start](#quick-start)
10
+
11
+ [Adding Routes](#adding-routes)
12
+
13
+ [Configuration](#configuration)
14
+
15
+ ## Features
16
+ - Type-checked throughout
17
+ - Completely customizable and open for extension
18
+ - Not opinionated; solves only the problems it is supposed to
19
+ - Easy to test
20
+ - Includes two documentation generators (Swagger, Stoplight)
21
+ - TypeScript client generator included
22
+ - Since you have OpenAPI, you can generate clients for any language you want
23
+
24
+ ## What is Strap-On OpenAPI?
25
+
26
+ OpenAPI is a standard for documenting your REST APIs. It's similar to JSDoc generators but with one major difference: it uses schemas that can be strictly typed and used for code generation.
27
+
28
+ This allows you to quickly generate clients for your API in most languages—pretty neat, right? The problem is those schemas are not very human-friendly and are hard to fill out by hand.
29
+
30
+ Documentation generators for OpenAPI allow you to send sample requests to your API and conveniently publish documentation for consumers.
31
+
32
+ Now let's talk about Zod. Zod is a validation library that allows you to infer types from validators. If an object passes validation with Zod, you can be sure it contains certain fields—even at compile time.
33
+
34
+ Zod works so well that I stopped using classes for models and DTOs in my own projects: I simply infer types from validators.
35
+
36
+ You might have the same idea I had some time ago: why not combine Zod and OpenAPI and make our API absolutely type-checked both on the frontend and backend? That would be a blast!
37
+
38
+ Strap-On OpenAPI is a lightweight, non-opinionated framework that allows you to do exactly that. It's highly customizable and easy to use, while providing fully type-checked context. You can forget about those "any" types that pop up here and there in your APIs.
39
+
40
+ The framework doesn't have any predefined middlewares (I don't even use such a concept) or excessive code. It has a few built-in errors and validators which I found helpful, and even those are made with the same utilities that are available to you.
41
+
42
+ Simply put, Strap-On OpenAPI is the glue that ties together OpenAPI, Zod, and Openapi-TS. And you are in charge of how your API is shaped—that's what sets this framework apart from tools like `GraphQL` and `tRPC`.
43
+
44
+ You can check out some sample code here:
45
+ https://github.com/Freddis/strap-on-openapi-samples
46
+
47
+ ## Disclaimer
48
+ Configuration is a bit clunky due to the huge amount of inferred types. But trust me, when you learn the basics (and there is no advanced level—it's really lightweight) you will be able to configure your API in just 5 minutes.
49
+
50
+ Keep in mind that in a real project, validators are defined in separate files and the production-grade code is significantly more elegant than what you see here in the documentation.
51
+
52
+ Normally, a route definition looks something like this:
53
+ ```typescript
54
+ export const upsertWorkouts = openApi.factory.createRoute({
55
+ method: OpenApiMethod.PUT,
56
+ type: ApiRouteType.User,
57
+ description: 'Updates or inserts workout for user',
58
+ path: '/',
59
+ validators: {
60
+ body: workoutUpsertDtoValidator,
61
+ response: workoutUpserResponseValidator
62
+ },
63
+ handler: async (ctx) => {
64
+ const result = await ctx.services.models.workout.upsert(ctx.viewer.id, ctx.params.body.items);
65
+ return {items: result};
66
+ },
67
+ });
68
+ ```
69
+
70
+ ## Installation
71
+
72
+ ```shell
73
+ npm install strap-on-openapi
74
+ ```
75
+
76
+ ## Quick Start
77
+ The idea behind Strap-On OpenAPI is that you don't need to bother with configuration right away. It is designed to be configured as you go. Fire it up, focus on your business logic, then add errors, routes, and contexts as you go.
78
+
79
+ Right now, Strap-On OpenAPI provides quickstart wrappers for Tanstack Start and Express.
80
+
81
+ ### Express
82
+
83
+ ```typescript
84
+ // file: index.ts
85
+ const app = express();
86
+ const openapi = OpenApi.create();
87
+ openapi.wrappers.express.createSwaggerRoute('/swagger', app);
88
+ openapi.wrappers.express.createOpenApiRootRoute(app);
89
+ app.listen(3000);
90
+ ```
91
+ And that's it. No need for any initial configuration—you already have Swagger documentation at http://localhost:3000/swagger.
92
+
93
+ Of course, since we haven't added routes, any attempt to send a request to `/api/something` will result in one of the built-in errors, but the API is already working.
94
+
95
+ ### Tanstack Start
96
+ Due to the slightly opinionated nature of Tanstack routing, we need more files:
97
+
98
+ Root Route:
99
+ ```typescript
100
+ // file: src/routes/api.ts
101
+ // Since we're using file routing, it's important that it matches the base path, which defaults to /api
102
+ import {createServerFileRoute} from '@tanstack/react-start/server';
103
+ import {openapi} from '../backend/utils/openApiInstance';
104
+
105
+ const methods = openapi.wrappers.tanstackStart.getOpenApiRootMethods();
106
+ export const ServerRoute = createServerFileRoute('/api').methods(methods);
107
+ ```
108
+ Schema Route:
109
+
110
+ ```typescript
111
+ // file: src/routes/schema.ts
112
+ import {createServerFileRoute} from '@tanstack/react-start/server';
113
+ import {openapi} from '../backend/utils/openApiInstance';
114
+
115
+ const methods = openapi.wrappers.tanstackStart.createShemaMethods();
116
+ export const ServerRoute = createServerFileRoute('/schema').methods(methods);
117
+ ```
118
+
119
+ Swagger:
120
+ ```typescript
121
+ // file: src/routes/swagger.ts
122
+ import {createServerFileRoute} from '@tanstack/react-start/server';
123
+ import {openapi} from '../backend/utils/openApiInstance';
124
+
125
+ const methods = openapi.wrappers.tanstackStart.createSwaggerMethods('/schema');
126
+ export const ServerRoute = createServerFileRoute('/swagger').methods(methods);
127
+ ```
128
+
129
+ Stoplight (Swagger Alternative):
130
+ ```typescript
131
+ // file: src/routes/stoplight.ts
132
+ import {createServerFileRoute} from '@tanstack/react-start/server';
133
+ import {openapi} from '../backend/utils/openApiInstance';
134
+
135
+ const methods = openapi.wrappers.tanstackStart.createStoplightMethods('/schema');
136
+ export const ServerRoute = createServerFileRoute('/stoplight').methods(methods);
137
+ ```
138
+
139
+ ### Custom
140
+ You don't have to use a wrapper to integrate OpenAPI with your framework. It's actually fairly simple to mount it on any framework: OpenAPI simply takes a Request object and returns this object:
141
+ ```typescript
142
+ {
143
+ status: number,
144
+ body: object
145
+ }
146
+ ```
147
+ Here's the code for the Express wrapper:
148
+ ```typescript
149
+ public createOpenApiRootRoute(expressApp: ExpressApp): void {
150
+ const route = this.service.getBasePath();
151
+ // Handler simply creates a basic Request object from Express Request
152
+ // and passes it to the OpenAPI instance (this.service)
153
+ const handler: ExpressHandler = async (req, res) => {
154
+ const emptyHeaders: Record<string, string> = {};
155
+ // Correcting the type for headers a little bit; you can do it with casting to any
156
+ const headers = Object.entries(req.headers).reduce((acc, val) => ({
157
+ ...acc,
158
+ ...(typeof val[1] === 'string' ? {[val[0]]: val[1]} : {}),
159
+ }), emptyHeaders);
160
+ const url = format({
161
+ protocol: req.protocol,
162
+ host: req.host,
163
+ pathname: req.originalUrl,
164
+ });
165
+ const openApiRequest = new Request(url, {
166
+ body: req.body,
167
+ headers: headers,
168
+ method: req.method,
169
+ });
170
+ const result = await this.service.processRootRoute(route, openApiRequest);
171
+ res.status(result.status).header('Content-Type', 'application/json').json(result.body);
172
+ };
173
+ // Assigning the same handler for every HTTP method matching our API base path
174
+ const regex = new RegExp(`${route}.*`);
175
+ expressApp.get(regex, handler);
176
+ expressApp.post(regex, handler);
177
+ expressApp.patch(regex, handler);
178
+ expressApp.delete(regex, handler);
179
+ expressApp.put(regex, handler);
180
+ }
181
+ ```
182
+ As you can see, it's not rocket science to integrate it with any framework.
183
+
184
+ ## Adding Routes
185
+
186
+ Now let's get deeper with Strap-On OpenAPI. Let's add some hot action.
187
+
188
+ We need to create a route and then add it to our OpenAPI instance. I recommend using a separate file for each route and one more file for the route map:
189
+ ```typescript
190
+ // file: src/openapi/getCars.ts
191
+ export const getCars = openapi.factory.createRoute({
192
+ type: OpenApiSampleRouteType.Public,
193
+ method: OpenApiMethods.GET,
194
+ path: "/get",
195
+ description: "Returns list of cars in stock",
196
+ validators: {
197
+ query: z.object({
198
+ make: z.string().optional().openapi({description: 'Car make filter'}),
199
+ }),
200
+ response: z.object({
201
+ name: z.string().openapi({description: 'Car name'}),
202
+ make: z.string().openapi({description: 'Make'}),
203
+ averageDriverIQ: z.number().openapi({description: "Average driver's IQ according to studies"}),
204
+ updatedAt: z.date().openapi({description: 'Last time the record was updated'}),
205
+ }).array().openapi({description: 'List of cars'}),
206
+ },
207
+ handler: async (ctx) => {
208
+ const m3 = {
209
+ name: "M3",
210
+ make: "BMW",
211
+ averageDriverIQ: 80,
212
+ updatedAt: new Date()
213
+ }
214
+ const supra = {
215
+ name: "Supra",
216
+ make: "Toyota",
217
+ averageDriverIQ: 130,
218
+ updatedAt: new Date()
219
+ }
220
+ const result = [m3, supra];
221
+ const filterValue = ctx.params.query.make;
222
+ if (!filterValue) {
223
+ return result;
224
+ }
225
+ return result.filter(x => x.make === filterValue);
226
+ }
227
+ })
228
+ ```
229
+ Now let's create a route map:
230
+ ```typescript
231
+ // file: src/openapi/routes.ts
232
+ import {OpenApiRouteMap, OpenApiSampleRouteType} from 'strap-on-openapi';
233
+ import {getCars} from './getCars';
234
+
235
+ export const openApiRoutes: OpenApiRouteMap<OpenApiSampleRouteType> = {
236
+ '/cars': [
237
+ getCars,
238
+ ]
239
+ }
240
+ ```
241
+ Finally, we need to add our route map to the OpenAPI instance:
242
+ ```typescript
243
+ // file: depends on your framework
244
+ openapi.addRouteMap(openApiRoutes);
245
+ ```
246
+
247
+ Now the new route should appear in your Swagger or Stoplight documentation, and you can send a request to test it out.
248
+
249
+ ```json
250
+ [
251
+ {
252
+ "name": "M3",
253
+ "make": "BMW",
254
+ "averageDriverIQ": 80,
255
+ "updatedAt": "2025-06-25T22:32:37.698Z"
256
+ },
257
+ {
258
+ "name": "Supra",
259
+ "make": "Toyota",
260
+ "averageDriverIQ": 130,
261
+ "updatedAt": "2025-06-25T22:32:37.698Z"
262
+ }
263
+ ]
264
+ ```
265
+
266
+ The code sample above is a little overloaded with information. In a developed application, routes look much cleaner than that. Here's an example from a real project:
267
+
268
+ ```typescript
269
+ export const upsertExercises = openApiInstance.factory.createRoute({
270
+ method: OpenApiMethods.PUT,
271
+ type: ApiRouteType.User,
272
+ description: 'Updates or inserts exercise in users personal library',
273
+ path: '/',
274
+ validators: {
275
+ body: z.object({
276
+ items: exerciseUpsertDtoValidator.array(),
277
+ }),
278
+ response: z.object({
279
+ items: exerciseValidator.array(),
280
+ }),
281
+ },
282
+ handler: async (ctx) => {
283
+ const result = await ctx.services.models.exercise.upsert(ctx.viewer.id, ctx.params.body.items);
284
+ return {items: result};
285
+ },
286
+ });
287
+ ```
288
+ And you can always write your own wrapper function to make it even less verbose if you need to.
289
+
290
+ ## Default Route Fields
291
+
292
+ - **Path**
293
+ Defines how the route looks. You can have parameters in the path using this syntax: `/getCar/{id}`
294
+
295
+ - **Method**
296
+ HTTP method (POST, GET, PUT, DELETE, PATCH). GET routes don't have body validators.
297
+
298
+ - **Description**
299
+ The description of the route, which will appear in the schema file and documentation. With default configuration, documentation check is forced and you will get a runtime error if it's empty (or too short).
300
+
301
+ - **Type**
302
+ Routes can be of different types, and those types are defined by you. If you don't provide route types, it defaults to `OpenApiSampleRouteType`, which only has public routes. Route types can have different extra fields in route definition and different contexts (middlewares).
303
+
304
+ Classic route types are public, user (which requires authorization), and admin (which has permissions on routes).
305
+
306
+ - **Validators**
307
+ Validators for input and output. This is an integral part of each route since you will only get access to the data that has been validated and only be able to return valid data in your response. The types are inferred from the shapes of the validators.
308
+
309
+ - **Handler**
310
+ Basically a controller for your route. It takes the context, which only contains body, path, and request parameters, and should return the response that fits the shape of the response validator.
311
+
312
+ It's intentional that you don't get headers and cookies here. By default, you only get the minimal viable things you need in order to operate. You can always add extra information by creating different contexts for your route types.
313
+
314
+ ## Configuration
315
+
316
+ Strap-On OpenAPI comes with a default configuration that covers basic errors and provides public route types. You can start with that, but eventually you will grow out of it.
317
+
318
+ There are two ways to configure Strap-On OpenAPI:
319
+ 1. Inferred config (recommended at the beginning)
320
+ 2. Implement OpenApiConfig interface
321
+
322
+ ### Inferred configuration
323
+ `OpenApi` is packaged with a builder that allows you to configure OpenAPI with a number of chained calls to various configuration methods.
324
+
325
+ Some calls are optional and some are required. If you're using a modern IDE such as VSCode, you can simply follow Intellisense. Each configuration call returns a new instance of the builder with specific updated methods. The `create()` method finalizes the build process and returns an OpenApi instance.
326
+
327
+ Initially, you have these options:
328
+
329
+ 1. `customizeErrors()`
330
+ Allows you to configure different error types, their response shapes, and lets you create the error handler that serves configured responses.
331
+ 2. `customizeRoutes()`
332
+ Allows you to configure different route types and their settings: additional route fields, contexts, authentication methods.
333
+ 3. `defineGlobalConfig()`
334
+ Allows you to configure general settings such as the base path where the API handles requests, servers where this API is available, and other miscellaneous settings.
335
+ 4. `create()`
336
+ Creates an instance of OpenApi.
337
+
338
+ These calls depend on each other and have to be called in the same order as they've been written above. You can call any of these methods, but if you called `customizeRoutes()`, you won't be able to call `customizeErrors()`.
339
+
340
+ This is done in such a manner to allow the TypeScript compiler to correctly pick up inferred types.
341
+
342
+ ### Global Configuration
343
+ ```typescript
344
+ const api = OpenApi.builder.defineGlobalConfig({
345
+ basePath: "/my-custom-api-path",
346
+ apiName: 'My Cool API',
347
+ servers: [
348
+ {
349
+ description: 'Local',
350
+ url: 'http://localhost:3000/my-custom-api-path',
351
+ },
352
+ {
353
+ description: 'Prod',
354
+ url: 'https://mydomain.com/my-custom-api-path',
355
+ }
356
+ ],
357
+ logLevel: OpenApiLogLevel.All,
358
+ skipDescriptionsCheck: false
359
+ }).create()
360
+ ```
361
+ If you override here, don't forget to fill in servers. Documentation generators come with playgrounds. These allow you to quickly test your API.
362
+
363
+ ### Configuring Routes
364
+ Let's create custom route types for authenticated users:
365
+ ```typescript
366
+ enum ApiRouteType {
367
+ Public = 'Public',
368
+ Member = 'Member',
369
+ }
370
+
371
+ export const api = OpenApi.builder.customizeRoutes(
372
+ ApiRouteType
373
+ ).defineRoutes({
374
+ [ApiRouteType.Public]: {
375
+ authorization: false,
376
+ },
377
+ [ApiRouteType.Member]: {
378
+ authorization: true, // only affects schema
379
+ }
380
+ }).create()
381
+ ```
382
+ After the `customizeRoutes()` call, we can define extra properties for our routes with Zod validators. Let's block access of the members to premium areas.
383
+
384
+ ```typescript
385
+ // Defining different subscription types for our members
386
+ enum Subscription {
387
+ Free = 'Free',
388
+ Premium = 'Premium',
389
+ }
390
+ export const api = OpenApi.builder.customizeRoutes(
391
+ ApiRouteType
392
+ ).defineRouteExtraProps({
393
+ [ApiRouteType.Public]: undefined,
394
+ [ApiRouteType.Member]: z.object({
395
+ subscription: z.nativeEnum(Subscription)
396
+ })
397
+ }).defineRoutes({
398
+ [ApiRouteType.Public]: {
399
+ authorization: false,
400
+ },
401
+ [ApiRouteType.Member]: {
402
+ authorization: true,
403
+ }
404
+ }).create();
405
+
406
+ // Now we can fill in subscription on member routes
407
+ const route = api.factory.createRoute({
408
+ type: ApiRouteType.Member,
409
+ method: OpenApiMethod.GET,
410
+ path: "/premium-analytics",
411
+ description: "Analytics for premium members",
412
+ validators: premiumAnalyticsValidator,
413
+ handler: function (context) {
414
+ // code that serves premium analytics to the member
415
+ },
416
+ subscription: Subscription.Premium // now we have this field here
417
+ })
418
+ ```
419
+ At the moment, this example is incomplete. Let's use our new field in the middleware that serves the context for member routes. It's called the context factory and it receives one parameter that contains information about the route, request objects, and some other properties.
420
+
421
+ ```typescript
422
+ export const api = OpenApi.builder.customizeRoutes(
423
+ ApiRouteType
424
+ ).defineRouteExtraProps({
425
+ [ApiRouteType.Public]: undefined,
426
+ [ApiRouteType.Member]: z.object({
427
+ subscription: z.nativeEnum(Subscription)
428
+ })
429
+ }).defineRouteContexts({
430
+ [ApiRouteType.Public]: () => Promise.resolve({}),
431
+ [ApiRouteType.Member]: async (ctx) => {
432
+ // obtaining user
433
+ const user: User | null = getUserFromRequest(ctx.request)
434
+ if (!user) {
435
+ throw new UnauthorizedError();
436
+ }
437
+ if (ctx.route.subscription !== user.subscription) {
438
+ throw new SubscriptionMismatchError(user);
439
+ }
440
+ // this object will be accessible in each Member route
441
+ return {user}
442
+ }
443
+ }).defineRoutes({
444
+ [ApiRouteType.Public]: {
445
+ authorization: false,
446
+ },
447
+ [ApiRouteType.Member]: {
448
+ authorization: true,
449
+ }
450
+ }).create();
451
+
452
+ const route = api.factory.createRoute({
453
+ type: ApiRouteType.Member,
454
+ method: OpenApiMethod.GET,
455
+ path: "/me",
456
+ description: "Analytics for premium members",
457
+ validators: userValidator,
458
+ handler: function (context) {
459
+ // the context values are visible in the routes
460
+ return context.user
461
+ },
462
+ subscription: Subscription.Free
463
+ })
464
+
465
+ ```
466
+ Note that route functions depend on each other and have to be called in this order:
467
+ 1. customizeRoutes()
468
+ 2. defineRouteExtraProps()
469
+ 3. defineRouteContexts()
470
+ 4. defineRoutes()
471
+
472
+ ### Configuring Errors
473
+ The configuration of errors starts with defining the Error Enum that contains all possible kinds of error responses you plan to use. You can go with as few as one error.
474
+
475
+ At this stage, it's important to recognize that we have two distinct entities here:
476
+ 1. Errors. To create an error in your service or route handler, you simply need to throw an Error there. Any normal JavaScript throwable object will do fine.
477
+ 2. Error responses. You can have a number of error responses for each kind of route.
478
+
479
+ With that said, you can see that you don't have to list all possible kinds of errors in your Error Enum—only errors that have a unique type of response or HTTP status.
480
+
481
+ > [!NOTE]
482
+ > Every error that can be thrown corresponds to one or multiple error responses. Whatever happens during API call processing, the consumer will
483
+ > always receive a response. That's why Strap-On OpenAPI requires at least one error response to be defined: it has to have the default error.
484
+
485
+ The `customizeErrors()` call will set you on the path of configuring errors. Similar to `customizeRoutes()`, you won't be able to call `create()` until you have provided everything required for the API to function properly.
486
+
487
+ Then we need to define responses for these errors by calling `defineErrors()`. It's done by setting the correct HTTP statuses, providing descriptions, and the validators for errors.
488
+ ```typescript
489
+ export enum AppErrorType {
490
+ Unknown = "Unknown",
491
+ Unauthorized = "Unauthorized",
492
+ ActionError = "ActionError",
493
+ }
494
+ export const openApi = OpenApi.builder.customizeErrors(
495
+ AppErrorType
496
+ ).defineErrors({
497
+ [AppErrorType.Unknown]: {
498
+ status: "500",
499
+ description: "Unknown Error",
500
+ responseValidator: z.object({
501
+ code: z.literal(AppErrorType.Unknown),
502
+ }),
503
+ },
504
+ [AppErrorType.ActionError]: {
505
+ status: "400",
506
+ description: "Error with human-readable explanation of what went wrong. Usually happens on action endpoints (i.e. login).",
507
+ responseValidator: z.object({
508
+ code: z.literal(AppErrorType.ActionError),
509
+ humanReadable: z.string(),
510
+ }),
511
+ },
512
+ [AppErrorType.Unauthorized]: {
513
+ status: "401",
514
+ description: "Unauthorized Error",
515
+ responseValidator: z.object({
516
+ code: z.literal(AppErrorType.Unauthorized),
517
+ }),
518
+ },
519
+ })
520
+ ```
521
+ After this is done, you will be prompted to call `defineDefaultError()`. This call is designed to configure the error that is going to be output to the consumer in case something unexpected happens. Naturally, it should correspond to one of your errors with HTTP status 500. It takes an object that consists of a value of the Error enum and the response that corresponds to its validator.
522
+
523
+ ```typescript
524
+ export const openApi = OpenApi.builder.customizeErrors(
525
+ AppErrorType
526
+ ).defineErrors({
527
+ ...
528
+ })
529
+ .defineDefaultError({
530
+ code: AppErrorType.Unknown,
531
+ body: {
532
+ code: AppErrorType.Unknown, // this is not a duplication, it follows from the definition of response
533
+ // some developers may choose a different shape, not related to the backend error types
534
+ },
535
+ })
536
+ ```
537
+
538
+ > [!NOTE]
539
+ > Note that the interface of `defineDefaultError` forces you to use synchronous context. It's no coincidence: errors may happen during your own error handling. This approach guarantees that whatever happens, we always have a suitable response ready.
540
+
541
+ The last thing we need to do is write the error handler itself. Strap-On OpenAPI can't magically know what error to respond with; the best it can do is respond with the default error response.
542
+
543
+ Surprisingly enough, the last call related to error handling is `customizeGlobalConfig()`, which was already covered above. The reason why it's done this way is to allow you to tweak error handling when you work with `DefaultConfig`. The built-in error types are quite good, and many people may prefer to use them for a while before actually setting up their own error responses.
544
+
545
+ ```typescript
546
+ // adds nothing to the table, except allowing us to use instanceOf() on it
547
+ export class UnauthorizedError extends Error {}
548
+ export class ActionError extends Error {
549
+ protected code: ActionErrorCode;
550
+
551
+ constructor(code: ActionErrorCode) {
552
+ super();
553
+ this.code = code;
554
+ }
555
+
556
+ getCode() {
557
+ return this.code;
558
+ }
559
+ }
560
+ export const actionErrorDescriptions: Record<ActionErrorCode, string> = {
561
+ [ActionErrorCode.WrongCredentials]: "Email or Password is incorrect",
562
+ [ActionErrorCode.UserAlreadyExists]: "User with this email already exists",
563
+ [ActionErrorCode.UserPasswordNotConfirmed]: "Password confirmation is wrong",
564
+ [ActionErrorCode.InvalidDataInRequest]: "The data in request is missing or has wrong format",
565
+ };
566
+
567
+ export const openApi = OpenApi.builder.customizeErrors(
568
+ AppErrorType
569
+ ).defineErrors({
570
+ ...
571
+ })
572
+ .defineDefaultError({
573
+ ...
574
+ })
575
+ .defineGlobalConfig({
576
+ basePath: "/api",
577
+ handleError: (e) => {
578
+ // processing my custom authorization error
579
+ if (e instanceof UnauthorizedError) {
580
+ const body: UnauthorizedErrorResponse = {
581
+ code: AppErrorType.Unauthorized,
582
+ };
583
+ return { code: AppErrorType.Unauthorized, body };
584
+ }
585
+ // Here I chose to respond with action error response for built-in validation error
586
+ if (e instanceof OpenApiValidationError) {
587
+ const body: ActionErrorResponse = {
588
+ code: AppErrorType.ActionError,
589
+ humanReadable:
590
+ actionErrorDescriptions[ActionErrorCode.InvalidDataInRequest],
591
+ };
592
+ return { code: AppErrorType.ActionError, body };
593
+ }
594
+ // Processing my custom action errors
595
+ if (e instanceof ActionError) {
596
+ const code = e.getCode();
597
+ const body: ActionErrorResponse = {
598
+ code: AppErrorType.ActionError,
599
+ humanReadable: actionErrorDescriptions[code],
600
+ };
601
+ return { code: AppErrorType.ActionError, body };
602
+ }
603
+ // default response
604
+ const defaultResponse: UnknownErrorResponse = {
605
+ code: AppErrorType.Unknown,
606
+ };
607
+ return { code: AppErrorType.Unknown, body: defaultResponse };
608
+ },
609
+ })
610
+ .create();
611
+ ```
612
+ ### Built-in Errors
613
+ There are three kinds of built-in error responses:
614
+ 1. NotFound. Happens if the consumer tries to hit a route that doesn't exist. Keep in mind that this error may only happen
615
+ 2. ValidationFailed. This happens when the data sent in the request or response hasn't passed validation. If validation fails in the response, then the API won't output this data to the consumer. It will be converted to an Unknown Error response for security reasons.
616
+ 3. UnknownError. This is the default error that happens on exceptions that weren't properly handled.
617
+
618
+ Under default error configuration, each of these gets its own response.
619
+
620
+ All built-in errors inherit from the class `BuiltInError`, which has a `getCode()` method to specify the error code listed above. In your `handleError()`, you may check if the error is an instance of `OpenApiBuiltInError` or `OpenApiValidationError` and shape your response accordingly.
621
+
622
+ ## Path Math
623
+ Routing paths are defined as strings starting with `/`. Paths can be `/`, `/something`, `/something/something`, and so on. Each route has a path, and each RouteMap piece has a path. It's very convenient to nest paths, as it allows moving routes around using RouteMap quickly.
624
+
625
+ `/` is considered to be an empty path. Empty paths collapse (are ignored) when being added to each other.
626
+
627
+ `/ + / = /`
628
+
629
+ `/ + /something = /something`
630
+
631
+ You don't have to think twice when you decide on the shape of the routes. Just put `/` if you don't want this RoutePart to have any effect.
632
+
633
+ > [!NOTE]
634
+ > This is possible because it's a REST API and API routing is far more simplistic than website routing.
635
+
636
+ There are three kinds of RoutePath used for every route:
637
+ 1. Base path, which defines the path where OpenAPI sits relative to the domain name of the application. Usually, it's something like `/api` or `/api/v1`.
638
+ 2. Route group path, which defines the path for a group of routes. Usually, something like `/cars` or `/users`.
639
+ 3. Route path, which is the path of the route itself. It can be anything: an empty route `/`, a parameter `/${id}`, or an action `/get`.
640
+
641
+ The final route is the sum of all three pieces: base path + group path + route path. Something like `/api/v1/users/${id}`.
642
+
643
+ You don't have to utilize all three paths. It's up to you. It's absolutely possible to operate only with route paths by putting empty routes for other kinds