transit-kit 0.8.0 â 0.8.2
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.
- package/README.md +553 -312
- package/dist/generator/generateOpenApi.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Transit-Kit
|
|
2
2
|
|
|
3
3
|
[](https://github.com/D4rkr34lm/transit-kit/actions/workflows/ci.yml)
|
|
4
4
|
[](https://coveralls.io/github/D4rkr34lm/transit-kit?branch=main)
|
|
5
|
+
[](https://www.npmjs.com/package/transit-kit)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
5
9
|
|
|
6
|
-
A declarative TypeScript framework for building type-safe Express.js
|
|
10
|
+
A **declarative TypeScript framework** for building end-to-end type-safe REST APIs with Express.js and automatic OpenAPI documentation generation.
|
|
7
11
|
|
|
8
|
-
##
|
|
12
|
+
## Why Transit-Kit?
|
|
9
13
|
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
Transit-Kit brings modern type-safety and developer experience to Express.js without abandoning the familiar, battle-tested foundation you already know. It's built on minimalism:
|
|
15
|
+
|
|
16
|
+
- ðŊ **Express at its core** â Use the familiar Express API you already know
|
|
17
|
+
- ð **End-to-end type safety** â Full TypeScript inference from request to response
|
|
18
|
+
- ð **Auto-generated OpenAPI** â Documentation that stays in sync with your code
|
|
19
|
+
- â
**Zod validation** â Runtime type checking with zero boilerplate
|
|
20
|
+
- ðŠķ **Minimal overhead** â Thin layer over Express, not a complete rewrite
|
|
21
|
+
- **Declarative definitions** â Define endpoints declaratively with full type info
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **Node.js 22+**
|
|
26
|
+
- **TypeScript 5.9+**
|
|
17
27
|
|
|
18
28
|
## Installation
|
|
19
29
|
|
|
@@ -21,450 +31,681 @@ A declarative TypeScript framework for building type-safe Express.js APIs with a
|
|
|
21
31
|
npm install transit-kit
|
|
22
32
|
```
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
```bash
|
|
35
|
+
yarn add transit-kit
|
|
36
|
+
```
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add transit-kit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
27
43
|
|
|
28
44
|
```typescript
|
|
29
45
|
import { createServer } from "transit-kit/server";
|
|
46
|
+
import { createApiEndpointHandler } from "transit-kit/server";
|
|
47
|
+
import { z } from "zod";
|
|
30
48
|
|
|
49
|
+
// Create a server
|
|
31
50
|
const server = createServer({
|
|
32
51
|
port: 3000,
|
|
33
52
|
inDevMode: true,
|
|
34
|
-
logger: true,
|
|
53
|
+
logger: true,
|
|
35
54
|
});
|
|
55
|
+
|
|
56
|
+
// Define a simple endpoint
|
|
57
|
+
const helloEndpoint = createApiEndpointHandler(
|
|
58
|
+
{
|
|
59
|
+
meta: {
|
|
60
|
+
name: "sayHello",
|
|
61
|
+
group: "Greetings",
|
|
62
|
+
description: "Returns a greeting message",
|
|
63
|
+
},
|
|
64
|
+
path: "/hello/:name",
|
|
65
|
+
method: "get",
|
|
66
|
+
responseSchemas: {
|
|
67
|
+
200: {
|
|
68
|
+
type: "json",
|
|
69
|
+
schema: z.object({
|
|
70
|
+
message: z.string(),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
securitySchemes: [],
|
|
75
|
+
},
|
|
76
|
+
async ({ parameters }) => {
|
|
77
|
+
return {
|
|
78
|
+
code: 200,
|
|
79
|
+
data: {
|
|
80
|
+
message: `Hello, ${parameters.name}!`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Register and start
|
|
87
|
+
server.registerApiEndpoint(helloEndpoint);
|
|
88
|
+
server.start();
|
|
36
89
|
```
|
|
37
90
|
|
|
38
|
-
|
|
91
|
+
## Core Concepts
|
|
92
|
+
|
|
93
|
+
### 1. Endpoint Definitions
|
|
94
|
+
|
|
95
|
+
Every API endpoint in Transit-Kit is defined declaratively with full type information:
|
|
39
96
|
|
|
40
97
|
```typescript
|
|
41
|
-
|
|
42
|
-
|
|
98
|
+
const definition = {
|
|
99
|
+
meta: {
|
|
100
|
+
name: "createUser", // Unique operation ID
|
|
101
|
+
group: "Users", // OpenAPI tag
|
|
102
|
+
description: "Create a user" // Endpoint description
|
|
103
|
+
},
|
|
104
|
+
path: "/users", // Express-style path
|
|
105
|
+
method: "post", // HTTP method
|
|
106
|
+
requestBodySchema: z.object({ // Request validation (optional)
|
|
107
|
+
name: z.string(),
|
|
108
|
+
email: z.string().email(),
|
|
109
|
+
}),
|
|
110
|
+
querySchema: z.object({ // Query params validation (optional)
|
|
111
|
+
sendEmail: z.boolean().optional(),
|
|
112
|
+
}),
|
|
113
|
+
responseSchemas: { // All possible responses
|
|
114
|
+
201: {
|
|
115
|
+
type: "json",
|
|
116
|
+
schema: z.object({
|
|
117
|
+
id: z.string(),
|
|
118
|
+
name: z.string(),
|
|
119
|
+
email: z.string(),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
400: {
|
|
123
|
+
type: "json",
|
|
124
|
+
schema: z.object({
|
|
125
|
+
error: z.string(),
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
securitySchemes: [], // Auth schemes (if any)
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Type-Safe Handlers
|
|
134
|
+
|
|
135
|
+
Handlers automatically infer types from your endpoint definition:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const handler = async ({ body, query, parameters }) => {
|
|
139
|
+
// body is typed as { name: string, email: string }
|
|
140
|
+
// query is typed as { sendEmail?: boolean }
|
|
141
|
+
// parameters are inferred from the path pattern
|
|
142
|
+
|
|
143
|
+
const user = await createUserInDatabase(body);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
code: 201,
|
|
147
|
+
data: user, // Must match the response schema for code 201
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. Server Registration
|
|
153
|
+
|
|
154
|
+
Combine definitions and handlers, then register with your server:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const endpoint = createApiEndpointHandler(definition, handler);
|
|
158
|
+
server.registerApiEndpoint(endpoint);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Example: CRUD API
|
|
162
|
+
|
|
163
|
+
Here's a complete example showing a REST API for managing todos:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { createServer, createApiEndpointHandler } from "transit-kit/server";
|
|
167
|
+
import { z } from "zod";
|
|
168
|
+
|
|
169
|
+
const server = createServer({
|
|
170
|
+
port: 3000,
|
|
171
|
+
inDevMode: true,
|
|
172
|
+
logger: true,
|
|
173
|
+
});
|
|
43
174
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
175
|
+
// In-memory storage (use a real database in production)
|
|
176
|
+
const todos: Map<string, { id: string; title: string; completed: boolean }> = new Map();
|
|
177
|
+
|
|
178
|
+
// Schemas
|
|
179
|
+
const TodoSchema = z.object({
|
|
180
|
+
id: z.string(),
|
|
181
|
+
title: z.string(),
|
|
182
|
+
completed: z.boolean(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const CreateTodoSchema = z.object({
|
|
186
|
+
title: z.string().min(1),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// CREATE
|
|
190
|
+
const createTodo = createApiEndpointHandler(
|
|
46
191
|
{
|
|
47
192
|
meta: {
|
|
48
|
-
name: "
|
|
49
|
-
|
|
50
|
-
|
|
193
|
+
name: "createTodo",
|
|
194
|
+
group: "Todos",
|
|
195
|
+
description: "Create a new todo item",
|
|
196
|
+
},
|
|
197
|
+
path: "/todos",
|
|
198
|
+
method: "post",
|
|
199
|
+
requestBodySchema: CreateTodoSchema,
|
|
200
|
+
responseSchemas: {
|
|
201
|
+
201: {
|
|
202
|
+
type: "json",
|
|
203
|
+
schema: TodoSchema,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
securitySchemes: [],
|
|
207
|
+
},
|
|
208
|
+
async ({ body }) => {
|
|
209
|
+
const id = crypto.randomUUID();
|
|
210
|
+
const todo = {
|
|
211
|
+
id,
|
|
212
|
+
title: body.title,
|
|
213
|
+
completed: false,
|
|
214
|
+
};
|
|
215
|
+
todos.set(id, todo);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
code: 201,
|
|
219
|
+
data: todo,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// READ (List)
|
|
225
|
+
const listTodos = createApiEndpointHandler(
|
|
226
|
+
{
|
|
227
|
+
meta: {
|
|
228
|
+
name: "listTodos",
|
|
229
|
+
group: "Todos",
|
|
230
|
+
description: "Get all todo items",
|
|
51
231
|
},
|
|
232
|
+
path: "/todos",
|
|
52
233
|
method: "get",
|
|
53
|
-
path: "/users/:userId",
|
|
54
234
|
responseSchemas: {
|
|
55
235
|
200: {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
name: z.string(),
|
|
60
|
-
email: z.string().email(),
|
|
236
|
+
type: "json",
|
|
237
|
+
schema: z.object({
|
|
238
|
+
todos: z.array(TodoSchema),
|
|
61
239
|
}),
|
|
62
240
|
},
|
|
63
|
-
404: {}, // Empty response
|
|
64
241
|
},
|
|
242
|
+
securitySchemes: [],
|
|
65
243
|
},
|
|
66
|
-
async (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
244
|
+
async () => {
|
|
245
|
+
return {
|
|
246
|
+
code: 200,
|
|
247
|
+
data: {
|
|
248
|
+
todos: Array.from(todos.values()),
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
);
|
|
71
253
|
|
|
72
|
-
|
|
254
|
+
// READ (Single)
|
|
255
|
+
const getTodo = createApiEndpointHandler(
|
|
256
|
+
{
|
|
257
|
+
meta: {
|
|
258
|
+
name: "getTodo",
|
|
259
|
+
group: "Todos",
|
|
260
|
+
description: "Get a specific todo item",
|
|
261
|
+
},
|
|
262
|
+
path: "/todos/:id",
|
|
263
|
+
method: "get",
|
|
264
|
+
responseSchemas: {
|
|
265
|
+
200: {
|
|
266
|
+
type: "json",
|
|
267
|
+
schema: TodoSchema,
|
|
268
|
+
},
|
|
269
|
+
404: {
|
|
270
|
+
type: "json",
|
|
271
|
+
schema: z.object({ error: z.string() }),
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
securitySchemes: [],
|
|
275
|
+
},
|
|
276
|
+
async ({ parameters }) => {
|
|
277
|
+
const todo = todos.get(parameters.id);
|
|
278
|
+
|
|
279
|
+
if (!todo) {
|
|
73
280
|
return {
|
|
74
281
|
code: 404,
|
|
282
|
+
data: { error: "Todo not found" },
|
|
75
283
|
};
|
|
76
284
|
}
|
|
77
285
|
|
|
78
286
|
return {
|
|
79
287
|
code: 200,
|
|
80
|
-
|
|
81
|
-
json: user,
|
|
288
|
+
data: todo,
|
|
82
289
|
};
|
|
83
|
-
}
|
|
290
|
+
}
|
|
84
291
|
);
|
|
85
|
-
```
|
|
86
292
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
const createUserEndpoint = createApiEndpointHandler(
|
|
293
|
+
// UPDATE
|
|
294
|
+
const updateTodo = createApiEndpointHandler(
|
|
91
295
|
{
|
|
92
296
|
meta: {
|
|
93
|
-
name: "
|
|
94
|
-
|
|
95
|
-
|
|
297
|
+
name: "updateTodo",
|
|
298
|
+
group: "Todos",
|
|
299
|
+
description: "Update a todo item",
|
|
96
300
|
},
|
|
97
|
-
|
|
98
|
-
|
|
301
|
+
path: "/todos/:id",
|
|
302
|
+
method: "put",
|
|
99
303
|
requestBodySchema: z.object({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
age: z.number().min(18),
|
|
103
|
-
}),
|
|
104
|
-
querySchema: z.object({
|
|
105
|
-
notify: z.boolean().optional(),
|
|
304
|
+
title: z.string().optional(),
|
|
305
|
+
completed: z.boolean().optional(),
|
|
106
306
|
}),
|
|
107
307
|
responseSchemas: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
id: z.string(),
|
|
112
|
-
name: z.string(),
|
|
113
|
-
email: z.string().email(),
|
|
114
|
-
}),
|
|
308
|
+
200: {
|
|
309
|
+
type: "json",
|
|
310
|
+
schema: TodoSchema,
|
|
115
311
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
error: z.string(),
|
|
120
|
-
}),
|
|
312
|
+
404: {
|
|
313
|
+
type: "json",
|
|
314
|
+
schema: z.object({ error: z.string() }),
|
|
121
315
|
},
|
|
122
316
|
},
|
|
317
|
+
securitySchemes: [],
|
|
123
318
|
},
|
|
124
|
-
async (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
319
|
+
async ({ parameters, body }) => {
|
|
320
|
+
const todo = todos.get(parameters.id);
|
|
321
|
+
|
|
322
|
+
if (!todo) {
|
|
323
|
+
return {
|
|
324
|
+
code: 404,
|
|
325
|
+
data: { error: "Todo not found" },
|
|
326
|
+
};
|
|
327
|
+
}
|
|
128
328
|
|
|
129
|
-
|
|
130
|
-
|
|
329
|
+
const updated = {
|
|
330
|
+
...todo,
|
|
331
|
+
...body,
|
|
332
|
+
};
|
|
333
|
+
todos.set(parameters.id, updated);
|
|
131
334
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
335
|
+
return {
|
|
336
|
+
code: 200,
|
|
337
|
+
data: updated,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
);
|
|
135
341
|
|
|
342
|
+
// DELETE
|
|
343
|
+
const deleteTodo = createApiEndpointHandler(
|
|
344
|
+
{
|
|
345
|
+
meta: {
|
|
346
|
+
name: "deleteTodo",
|
|
347
|
+
group: "Todos",
|
|
348
|
+
description: "Delete a todo item",
|
|
349
|
+
},
|
|
350
|
+
path: "/todos/:id",
|
|
351
|
+
method: "delete",
|
|
352
|
+
responseSchemas: {
|
|
353
|
+
204: {
|
|
354
|
+
type: "json",
|
|
355
|
+
schema: z.object({}),
|
|
356
|
+
},
|
|
357
|
+
404: {
|
|
358
|
+
type: "json",
|
|
359
|
+
schema: z.object({ error: z.string() }),
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
securitySchemes: [],
|
|
363
|
+
},
|
|
364
|
+
async ({ parameters }) => {
|
|
365
|
+
const exists = todos.has(parameters.id);
|
|
366
|
+
|
|
367
|
+
if (!exists) {
|
|
136
368
|
return {
|
|
137
|
-
code:
|
|
138
|
-
|
|
139
|
-
json: newUser,
|
|
140
|
-
};
|
|
141
|
-
} catch (error) {
|
|
142
|
-
return {
|
|
143
|
-
code: 400,
|
|
144
|
-
dataType: "application/json",
|
|
145
|
-
json: { error: error.message },
|
|
369
|
+
code: 404,
|
|
370
|
+
data: { error: "Todo not found" },
|
|
146
371
|
};
|
|
147
372
|
}
|
|
148
|
-
},
|
|
149
|
-
);
|
|
150
|
-
```
|
|
151
373
|
|
|
152
|
-
|
|
374
|
+
todos.delete(parameters.id);
|
|
153
375
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
376
|
+
return {
|
|
377
|
+
code: 204,
|
|
378
|
+
data: {},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Register all endpoints
|
|
384
|
+
server.registerApiEndpoint(createTodo);
|
|
385
|
+
server.registerApiEndpoint(listTodos);
|
|
386
|
+
server.registerApiEndpoint(getTodo);
|
|
387
|
+
server.registerApiEndpoint(updateTodo);
|
|
388
|
+
server.registerApiEndpoint(deleteTodo);
|
|
157
389
|
|
|
158
390
|
server.start();
|
|
391
|
+
console.log("Server running on http://localhost:3000");
|
|
159
392
|
```
|
|
160
393
|
|
|
161
394
|
## Authentication
|
|
162
395
|
|
|
163
|
-
Transit-
|
|
396
|
+
Transit-Kit supports **Basic** and **Bearer** authentication schemes out of the box.
|
|
164
397
|
|
|
165
|
-
|
|
166
|
-
- Try all declared schemes in order and pick the first successful one
|
|
167
|
-
- Return `401 Unauthorized` automatically when authentication fails
|
|
168
|
-
- Inject the authenticated `caller` into your endpoint handler
|
|
398
|
+
### Basic Authentication
|
|
169
399
|
|
|
170
|
-
|
|
400
|
+
```typescript
|
|
401
|
+
import { createBasicAuthSchema } from "transit-kit/server";
|
|
171
402
|
|
|
172
|
-
|
|
403
|
+
// Define your user type
|
|
404
|
+
interface User {
|
|
405
|
+
id: string;
|
|
406
|
+
username: string;
|
|
407
|
+
role: string;
|
|
408
|
+
}
|
|
173
409
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
createServer,
|
|
178
|
-
} from "transit-kit/server";
|
|
179
|
-
import { createBasicAuthSchema } from "transit-kit/server/security/basicAuth";
|
|
180
|
-
import z from "zod";
|
|
181
|
-
|
|
182
|
-
// Define the shape of the authenticated caller
|
|
183
|
-
type Caller = { id: string; role: "admin" | "user" };
|
|
184
|
-
|
|
185
|
-
// Your validation logic (DB lookup, etc.)
|
|
186
|
-
const basicAuth = createBasicAuthSchema<Caller>(
|
|
187
|
-
"basic",
|
|
410
|
+
// Create an auth scheme
|
|
411
|
+
const basicAuth = createBasicAuthSchema<User>(
|
|
412
|
+
"basicAuth",
|
|
188
413
|
async (username, password) => {
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
method: "get",
|
|
200
|
-
path: "/me",
|
|
201
|
-
responseSchemas: {
|
|
202
|
-
200: {
|
|
203
|
-
dataType: "application/json",
|
|
204
|
-
dataSchema: z.object({ id: z.string(), role: z.enum(["admin", "user"]) }),
|
|
205
|
-
},
|
|
206
|
-
401: {},
|
|
207
|
-
},
|
|
208
|
-
// Enable authentication for this endpoint
|
|
209
|
-
securitySchemes: [basicAuth],
|
|
210
|
-
},
|
|
211
|
-
async (_req, { caller }) => {
|
|
212
|
-
// `caller` is typed from your security scheme
|
|
213
|
-
return { code: 200, dataType: "application/json", json: caller };
|
|
214
|
-
},
|
|
414
|
+
// Validate credentials (use your database)
|
|
415
|
+
if (username === "admin" && password === "secret") {
|
|
416
|
+
return {
|
|
417
|
+
id: "1",
|
|
418
|
+
username: "admin",
|
|
419
|
+
role: "admin",
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return null; // Invalid credentials
|
|
423
|
+
}
|
|
215
424
|
);
|
|
216
|
-
|
|
217
|
-
const server = createServer({ port: 3000, inDevMode: true, logger: true });
|
|
218
|
-
server.registerApiEndpoint(getProfile);
|
|
219
|
-
server.start();
|
|
220
425
|
```
|
|
221
426
|
|
|
222
|
-
### Bearer
|
|
223
|
-
|
|
224
|
-
Supports `Authorization: Bearer <token>`.
|
|
427
|
+
### Bearer Authentication
|
|
225
428
|
|
|
226
429
|
```typescript
|
|
227
|
-
import { createBearerAuthSchema } from "transit-kit/server
|
|
228
|
-
|
|
229
|
-
type Caller = { id: string; scopes: string[] };
|
|
430
|
+
import { createBearerAuthSchema } from "transit-kit/server";
|
|
230
431
|
|
|
231
|
-
const bearerAuth = createBearerAuthSchema<
|
|
232
|
-
"
|
|
432
|
+
const bearerAuth = createBearerAuthSchema<User>(
|
|
433
|
+
"bearerAuth",
|
|
233
434
|
async (token) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
435
|
+
// Validate token (e.g., JWT verification)
|
|
436
|
+
const user = await verifyJWT(token);
|
|
437
|
+
return user; // or null if invalid
|
|
438
|
+
}
|
|
237
439
|
);
|
|
440
|
+
```
|
|
238
441
|
|
|
239
|
-
|
|
442
|
+
### Protected Endpoints
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
const protectedEndpoint = createApiEndpointHandler(
|
|
240
446
|
{
|
|
241
|
-
meta: {
|
|
447
|
+
meta: {
|
|
448
|
+
name: "getProfile",
|
|
449
|
+
group: "Users",
|
|
450
|
+
description: "Get current user profile",
|
|
451
|
+
},
|
|
452
|
+
path: "/profile",
|
|
242
453
|
method: "get",
|
|
243
|
-
path: "/secret",
|
|
244
454
|
responseSchemas: {
|
|
245
|
-
200: {
|
|
246
|
-
|
|
455
|
+
200: {
|
|
456
|
+
type: "json",
|
|
457
|
+
schema: z.object({
|
|
458
|
+
id: z.string(),
|
|
459
|
+
username: z.string(),
|
|
460
|
+
role: z.string(),
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
401: {
|
|
464
|
+
type: "json",
|
|
465
|
+
schema: z.object({ error: z.string() }),
|
|
466
|
+
},
|
|
247
467
|
},
|
|
248
|
-
securitySchemes: [
|
|
249
|
-
},
|
|
250
|
-
async (_req, { caller }) => {
|
|
251
|
-
// Use `caller.scopes` for authorization decisions
|
|
252
|
-
return { code: 200, dataType: "application/json", json: { secret: "shh" } };
|
|
468
|
+
securitySchemes: [basicAuth], // Require authentication
|
|
253
469
|
},
|
|
470
|
+
async ({ caller }) => {
|
|
471
|
+
// caller is typed as User | null
|
|
472
|
+
if (!caller) {
|
|
473
|
+
return {
|
|
474
|
+
code: 401,
|
|
475
|
+
data: { error: "Unauthorized" },
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
code: 200,
|
|
481
|
+
data: caller, // Fully typed user object
|
|
482
|
+
};
|
|
483
|
+
}
|
|
254
484
|
);
|
|
485
|
+
|
|
486
|
+
server.registerApiEndpoint(protectedEndpoint);
|
|
255
487
|
```
|
|
256
488
|
|
|
257
|
-
### Multiple Schemes
|
|
489
|
+
### Multiple Auth Schemes
|
|
258
490
|
|
|
259
|
-
You can
|
|
491
|
+
You can support multiple authentication methods:
|
|
260
492
|
|
|
261
493
|
```typescript
|
|
262
494
|
const endpoint = createApiEndpointHandler(
|
|
263
495
|
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
path: "/hybrid",
|
|
267
|
-
responseSchemas: { 200: { dataType: "application/json", dataSchema: z.object({ ok: z.boolean() }) }, 401: {} },
|
|
268
|
-
securitySchemes: [basicAuth, bearerAuth],
|
|
269
|
-
},
|
|
270
|
-
async (_req, { caller }) => {
|
|
271
|
-
// `caller` resolves from whichever scheme authenticated successfully
|
|
272
|
-
return { code: 200, dataType: "application/json", json: { ok: true } };
|
|
496
|
+
// ... other config
|
|
497
|
+
securitySchemes: [basicAuth, bearerAuth], // Accept either
|
|
273
498
|
},
|
|
499
|
+
async ({ caller }) => {
|
|
500
|
+
// caller will be set if any scheme succeeds
|
|
501
|
+
// ...
|
|
502
|
+
}
|
|
274
503
|
);
|
|
275
504
|
```
|
|
276
505
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
- Authentication middleware is built automatically per-endpoint when `securitySchemes` are present.
|
|
280
|
-
- Internals use `authenticate()` from [src/server/security/SecuritySchema.ts](src/server/security/SecuritySchema.ts) to try each scheme.
|
|
281
|
-
- Basic auth reads and decodes the header in [src/server/security/basicAuth.ts](src/server/security/basicAuth.ts).
|
|
282
|
-
- Bearer auth reads the header in [src/server/security/bearerAuth.ts](src/server/security/bearerAuth.ts).
|
|
283
|
-
- On success, the authenticated `caller` is stored and passed to your handler as `extractedRequestData.caller`.
|
|
284
|
-
- On failure, the framework responds with `401` and a JSON `{ message: "Unauthorized" }` from [src/server/middleware/auth.ts](src/server/middleware/auth.ts).
|
|
506
|
+
## OpenAPI Documentation
|
|
285
507
|
|
|
286
|
-
|
|
508
|
+
Transit-Kit automatically generates OpenAPI 3.0 documentation from your endpoint definitions.
|
|
287
509
|
|
|
288
|
-
|
|
510
|
+
```typescript
|
|
511
|
+
import { generateOpenApiDoc } from "transit-kit/generator";
|
|
512
|
+
|
|
513
|
+
const openApiDoc = await generateOpenApiDoc(server, {
|
|
514
|
+
title: "My API",
|
|
515
|
+
version: "1.0.0",
|
|
516
|
+
description: "A type-safe REST API built with Transit-Kit",
|
|
517
|
+
servers: [
|
|
518
|
+
{
|
|
519
|
+
url: "http://localhost:3000",
|
|
520
|
+
description: "Development server",
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
url: "https://api.example.com",
|
|
524
|
+
description: "Production server",
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
contact: {
|
|
528
|
+
name: "API Support",
|
|
529
|
+
email: "support@example.com",
|
|
530
|
+
},
|
|
531
|
+
license: {
|
|
532
|
+
name: "MIT",
|
|
533
|
+
url: "https://opensource.org/licenses/MIT",
|
|
534
|
+
},
|
|
535
|
+
});
|
|
289
536
|
|
|
290
|
-
|
|
537
|
+
// Serve the OpenAPI spec
|
|
538
|
+
server.expressApp.get("/openapi.json", (req, res) => {
|
|
539
|
+
res.json(openApiDoc);
|
|
540
|
+
});
|
|
291
541
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
dataType: "application/json",
|
|
296
|
-
json: { message: "Success", data: myData },
|
|
297
|
-
};
|
|
542
|
+
// Or write to file
|
|
543
|
+
import { writeFileSync } from "fs";
|
|
544
|
+
writeFileSync("./openapi.json", JSON.stringify(openApiDoc, null, 2));
|
|
298
545
|
```
|
|
299
546
|
|
|
300
|
-
|
|
547
|
+
The generated OpenAPI document includes:
|
|
548
|
+
- All endpoints with request/response schemas
|
|
549
|
+
- Path and query parameters
|
|
550
|
+
- Request body validation schemas
|
|
551
|
+
- Security requirements
|
|
552
|
+
- Response schemas for all status codes
|
|
301
553
|
|
|
302
|
-
|
|
554
|
+
You can use this with tools like:
|
|
555
|
+
- **Swagger UI** for interactive documentation
|
|
556
|
+
- **Postman** for API testing
|
|
557
|
+
- **OpenAPI Generator** for client SDK generation
|
|
303
558
|
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
code: 204,
|
|
307
|
-
};
|
|
308
|
-
```
|
|
559
|
+
## API Reference
|
|
309
560
|
|
|
310
|
-
|
|
561
|
+
### Server
|
|
311
562
|
|
|
312
|
-
|
|
563
|
+
#### `createServer(config: ServerConfig): Server`
|
|
313
564
|
|
|
314
|
-
|
|
565
|
+
Creates a new Transit-Kit server instance.
|
|
315
566
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
567
|
+
**Config Options:**
|
|
568
|
+
- `port: number` â Port to listen on
|
|
569
|
+
- `inDevMode: boolean` â Enable development mode (detailed logging)
|
|
570
|
+
- `logger: Logger | boolean` â Custom logger or true/false for console/no logging
|
|
319
571
|
|
|
320
|
-
|
|
572
|
+
**Returns:** Server instance with:
|
|
573
|
+
- `expressApp: Application` â Underlying Express app
|
|
574
|
+
- `registerApiEndpoint(endpoint)` â Register an API endpoint
|
|
575
|
+
- `start()` â Start the server
|
|
576
|
+
- `endpointDefinitions` â Array of registered endpoint definitions
|
|
321
577
|
|
|
322
|
-
|
|
323
|
-
- `-t, --target <path>`: Target path to search for endpoint definitions (default: `.`)
|
|
578
|
+
### Endpoint Creation
|
|
324
579
|
|
|
325
|
-
|
|
580
|
+
#### `createApiEndpointHandler(definition, handler)`
|
|
326
581
|
|
|
327
|
-
|
|
328
|
-
import { generateOpenApiDoc } from "transit-kit/cli";
|
|
582
|
+
Creates a type-safe API endpoint.
|
|
329
583
|
|
|
330
|
-
|
|
331
|
-
|
|
584
|
+
**Parameters:**
|
|
585
|
+
- `definition` â Endpoint definition object
|
|
586
|
+
- `handler` â Async function handling the request
|
|
332
587
|
|
|
333
|
-
|
|
588
|
+
**Handler receives:**
|
|
589
|
+
- `request` â Express Request object
|
|
590
|
+
- `response` â Express Response object
|
|
591
|
+
- `parameters` â Type-safe path parameters
|
|
592
|
+
- `query` â Type-safe query parameters
|
|
593
|
+
- `body` â Type-safe request body
|
|
594
|
+
- `caller` â Authenticated user (if auth is enabled)
|
|
334
595
|
|
|
335
|
-
|
|
336
|
-
- Request/response schemas
|
|
337
|
-
- Path parameters
|
|
338
|
-
- Query parameters
|
|
339
|
-
- Request body schemas
|
|
340
|
-
- Response schemas for all status codes
|
|
596
|
+
### Authentication
|
|
341
597
|
|
|
342
|
-
|
|
598
|
+
#### `createBasicAuthSchema<Caller>(name, validateCaller)`
|
|
343
599
|
|
|
344
|
-
|
|
600
|
+
Creates a Basic authentication scheme.
|
|
345
601
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
port: number; // Port to listen on
|
|
350
|
-
logger: Logger | boolean; // Logger instance or boolean to enable/disable
|
|
351
|
-
}
|
|
352
|
-
```
|
|
602
|
+
**Parameters:**
|
|
603
|
+
- `name: string` â Unique name for the scheme
|
|
604
|
+
- `validateCaller: (username, password) => Promise<Caller | null>` â Validation function
|
|
353
605
|
|
|
354
|
-
|
|
606
|
+
#### `createBearerAuthSchema<Caller>(name, validateCaller)`
|
|
355
607
|
|
|
356
|
-
|
|
608
|
+
Creates a Bearer token authentication scheme.
|
|
357
609
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
/* custom logging */
|
|
362
|
-
},
|
|
363
|
-
error: (message: string) => {
|
|
364
|
-
/* custom error logging */
|
|
365
|
-
},
|
|
366
|
-
// ... other console methods
|
|
367
|
-
};
|
|
610
|
+
**Parameters:**
|
|
611
|
+
- `name: string` â Unique name for the scheme
|
|
612
|
+
- `validateCaller: (token) => Promise<Caller | null>` â Validation function
|
|
368
613
|
|
|
369
|
-
|
|
370
|
-
port: 3000,
|
|
371
|
-
inDevMode: false,
|
|
372
|
-
logger: customLogger,
|
|
373
|
-
});
|
|
374
|
-
```
|
|
614
|
+
### OpenAPI Generation
|
|
375
615
|
|
|
376
|
-
|
|
616
|
+
#### `generateOpenApiDoc(server, options): Promise<OpenAPIV3.Document>`
|
|
377
617
|
|
|
378
|
-
|
|
618
|
+
Generates OpenAPI 3.0 documentation.
|
|
379
619
|
|
|
380
|
-
|
|
620
|
+
**Parameters:**
|
|
621
|
+
- `server: Server` â Your Transit-Kit server instance
|
|
622
|
+
- `options: GeneratorOptions` â OpenAPI metadata
|
|
381
623
|
|
|
382
|
-
|
|
624
|
+
**Options:**
|
|
625
|
+
- `title: string` â API title
|
|
626
|
+
- `version: string` â API version
|
|
627
|
+
- `description?: string` â API description
|
|
628
|
+
- `servers?: ServerObject[]` â Server URLs
|
|
629
|
+
- `contact?: ContactObject` â Contact information
|
|
630
|
+
- `license?: LicenseObject` â License information
|
|
383
631
|
|
|
384
|
-
|
|
632
|
+
## Response Types
|
|
385
633
|
|
|
386
|
-
|
|
634
|
+
Transit-Kit currently supports JSON responses:
|
|
387
635
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
-
|
|
393
|
-
|
|
636
|
+
```typescript
|
|
637
|
+
{
|
|
638
|
+
type: "json",
|
|
639
|
+
schema: z.object({ /* your schema */ }),
|
|
640
|
+
headers?: ["X-Custom-Header"], // Optional custom headers
|
|
641
|
+
}
|
|
642
|
+
```
|
|
394
643
|
|
|
395
|
-
|
|
644
|
+
When using custom headers in your response schema, you must include them in the handler response:
|
|
396
645
|
|
|
397
|
-
|
|
646
|
+
```typescript
|
|
647
|
+
return {
|
|
648
|
+
code: 200,
|
|
649
|
+
data: { /* ... */ },
|
|
650
|
+
headers: {
|
|
651
|
+
"X-Custom-Header": "value",
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
```
|
|
398
655
|
|
|
399
|
-
|
|
656
|
+
## Accessing Express Features
|
|
400
657
|
|
|
401
|
-
|
|
658
|
+
Since Transit-Kit is built on Express, you can access the underlying Express app:
|
|
402
659
|
|
|
403
|
-
|
|
660
|
+
```typescript
|
|
661
|
+
const server = createServer({ /* ... */ });
|
|
404
662
|
|
|
405
|
-
|
|
663
|
+
// Add custom middleware
|
|
664
|
+
server.expressApp.use(cors());
|
|
665
|
+
server.expressApp.use(helmet());
|
|
406
666
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
import z from "zod";
|
|
667
|
+
// Static files
|
|
668
|
+
server.expressApp.use(express.static("public"));
|
|
410
669
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
inDevMode: true,
|
|
415
|
-
logger: true,
|
|
670
|
+
// Custom routes
|
|
671
|
+
server.expressApp.get("/health", (req, res) => {
|
|
672
|
+
res.json({ status: "ok" });
|
|
416
673
|
});
|
|
674
|
+
```
|
|
417
675
|
|
|
418
|
-
|
|
419
|
-
const listUsersEndpoint = createApiEndpointHandler(
|
|
420
|
-
{
|
|
421
|
-
meta: {
|
|
422
|
-
name: "List Users",
|
|
423
|
-
description: "Get a list of all users",
|
|
424
|
-
group: "Users",
|
|
425
|
-
},
|
|
426
|
-
method: "get",
|
|
427
|
-
path: "/users",
|
|
428
|
-
querySchema: z.object({
|
|
429
|
-
page: z.number().optional(),
|
|
430
|
-
limit: z.number().optional(),
|
|
431
|
-
}),
|
|
432
|
-
responseSchemas: {
|
|
433
|
-
200: {
|
|
434
|
-
dataType: "application/json",
|
|
435
|
-
dataSchema: z.array(
|
|
436
|
-
z.object({
|
|
437
|
-
id: z.string(),
|
|
438
|
-
name: z.string(),
|
|
439
|
-
}),
|
|
440
|
-
),
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
async (request) => {
|
|
445
|
-
const { page = 1, limit = 10 } = request.query;
|
|
446
|
-
const users = await fetchUsers(page, limit);
|
|
676
|
+
## Project Structure
|
|
447
677
|
|
|
448
|
-
|
|
449
|
-
code: 200,
|
|
450
|
-
dataType: "application/json",
|
|
451
|
-
json: users,
|
|
452
|
-
};
|
|
453
|
-
},
|
|
454
|
-
);
|
|
678
|
+
Transit-Kit exports two main modules:
|
|
455
679
|
|
|
456
|
-
server
|
|
457
|
-
|
|
680
|
+
- **`transit-kit/server`** â Server creation, endpoint handlers, and authentication
|
|
681
|
+
- **`transit-kit/generator`** â OpenAPI documentation generation
|
|
682
|
+
|
|
683
|
+
## TypeScript Configuration
|
|
684
|
+
|
|
685
|
+
For the best experience, ensure your `tsconfig.json` includes:
|
|
686
|
+
|
|
687
|
+
```json
|
|
688
|
+
{
|
|
689
|
+
"compilerOptions": {
|
|
690
|
+
"strict": true,
|
|
691
|
+
"esModuleInterop": true,
|
|
692
|
+
"moduleResolution": "node",
|
|
693
|
+
"target": "ES2022",
|
|
694
|
+
"module": "ES2022"
|
|
695
|
+
}
|
|
696
|
+
}
|
|
458
697
|
```
|
|
459
698
|
|
|
460
699
|
## License
|
|
461
700
|
|
|
462
|
-
MIT
|
|
701
|
+
MIT ÂĐ [D4rkr34lm](https://github.com/D4rkr34lm)
|
|
463
702
|
|
|
464
703
|
## Contributing
|
|
465
704
|
|
|
466
705
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
467
706
|
|
|
468
|
-
##
|
|
707
|
+
## Links
|
|
469
708
|
|
|
470
|
-
D4rkr34lm
|
|
709
|
+
- [GitHub Repository](https://github.com/D4rkr34lm/transit-kit)
|
|
710
|
+
- [npm Package](https://www.npmjs.com/package/transit-kit)
|
|
711
|
+
- [Report Issues](https://github.com/D4rkr34lm/transit-kit/issues)
|
|
@@ -17,7 +17,10 @@ function extractPathAndParameters(path) {
|
|
|
17
17
|
return { openApiPath, parameters };
|
|
18
18
|
}
|
|
19
19
|
function extractQueryParameters(querySchema) {
|
|
20
|
-
const querySchemaObject = z.toJSONSchema(querySchema, {
|
|
20
|
+
const querySchemaObject = z.toJSONSchema(querySchema, {
|
|
21
|
+
io: "input",
|
|
22
|
+
target: "openapi-3.0",
|
|
23
|
+
});
|
|
21
24
|
if (querySchemaObject.properties) {
|
|
22
25
|
return Object.entries(querySchemaObject.properties).map(([name, schema]) => ({
|
|
23
26
|
name: name,
|
|
@@ -65,6 +68,7 @@ function translateToOpenAPIPathItem(definition) {
|
|
|
65
68
|
"application/json": {
|
|
66
69
|
schema: z.toJSONSchema(requestBodySchema, {
|
|
67
70
|
io: "input",
|
|
71
|
+
target: "openapi-3.0",
|
|
68
72
|
}), // Type assertion
|
|
69
73
|
},
|
|
70
74
|
},
|
|
@@ -76,7 +80,10 @@ function translateToOpenAPIPathItem(definition) {
|
|
|
76
80
|
.map(([statusCode, responseDef]) => {
|
|
77
81
|
if (isJsonResponseSchema(responseDef)) {
|
|
78
82
|
const zodSchema = responseDef.schema;
|
|
79
|
-
const responseSchema = z.toJSONSchema(zodSchema, {
|
|
83
|
+
const responseSchema = z.toJSONSchema(zodSchema, {
|
|
84
|
+
io: "input",
|
|
85
|
+
target: "openapi-3.0",
|
|
86
|
+
});
|
|
80
87
|
return {
|
|
81
88
|
[statusCode]: {
|
|
82
89
|
description: `Response for status code ${statusCode}`,
|