@rabstack/rab-api 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.
- package/README.md +393 -0
- package/index.cjs.d.ts +1 -0
- package/index.cjs.js +1105 -0
- package/index.esm.d.ts +966 -0
- package/index.esm.js +1070 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# rab-api
|
|
2
|
+
|
|
3
|
+
A TypeScript REST API framework built on Express.js with decorator-based routing, dependency injection, and built-in validation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 Decorator-based routing with TypeScript
|
|
8
|
+
- 🔒 Built-in JWT authentication
|
|
9
|
+
- ✅ Request validation with Joi schemas
|
|
10
|
+
- 💉 Dependency injection (TypeDI)
|
|
11
|
+
- 🔐 Role-based access control
|
|
12
|
+
- 📝 Full TypeScript type safety
|
|
13
|
+
- 🚀 Production-ready
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install rab-api
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Peer dependencies:**
|
|
22
|
+
```bash
|
|
23
|
+
npm install express joi typedi reflect-metadata jsonwebtoken compose-middleware
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
**1. Create a controller:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Get, RabApiGet, GetController } from 'rab-api';
|
|
32
|
+
|
|
33
|
+
type ControllerT = GetController<{ status: string }>;
|
|
34
|
+
|
|
35
|
+
@Get('/health')
|
|
36
|
+
export class HealthCheck implements RabApiGet<ControllerT> {
|
|
37
|
+
handler: ControllerT['request'] = async () => {
|
|
38
|
+
return { status: 'ok' };
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Bootstrap your app:**
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { RabApi } from 'rab-api';
|
|
47
|
+
import express from 'express';
|
|
48
|
+
|
|
49
|
+
const app = RabApi.createApp({
|
|
50
|
+
auth: {
|
|
51
|
+
jwt: {
|
|
52
|
+
secret_key: process.env.JWT_SECRET!,
|
|
53
|
+
algorithms: ['HS256'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
|
|
60
|
+
app.route({
|
|
61
|
+
basePath: '/api',
|
|
62
|
+
controllers: [HealthCheck],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.listen(3000);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Core Concepts
|
|
69
|
+
|
|
70
|
+
### Controllers
|
|
71
|
+
|
|
72
|
+
Controllers handle HTTP requests using decorators:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { Post, RabApiPost, PostController } from 'rab-api';
|
|
76
|
+
import * as Joi from 'joi';
|
|
77
|
+
|
|
78
|
+
type CreateUserBody = { name: string; email: string };
|
|
79
|
+
type UserResponse = { id: string; name: string; email: string };
|
|
80
|
+
type ControllerT = PostController<CreateUserBody, UserResponse>;
|
|
81
|
+
|
|
82
|
+
const schema = Joi.object({
|
|
83
|
+
name: Joi.string().required(),
|
|
84
|
+
email: Joi.string().email().required(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
@Post('/users', { bodySchema: schema })
|
|
88
|
+
export class CreateUser implements RabApiPost<ControllerT> {
|
|
89
|
+
handler: ControllerT['request'] = async (request) => {
|
|
90
|
+
return { id: '1', ...request.body };
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Available decorators:**
|
|
96
|
+
- `@Get(path, options?)` - GET requests
|
|
97
|
+
- `@Post(path, options?)` - POST requests
|
|
98
|
+
- `@Put(path, options?)` - PUT requests
|
|
99
|
+
- `@Patch(path, options?)` - PATCH requests
|
|
100
|
+
- `@Delete(path, options?)` - DELETE requests
|
|
101
|
+
|
|
102
|
+
### Dependency Injection
|
|
103
|
+
|
|
104
|
+
Controllers support constructor injection via TypeDI:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { Injectable } from 'rab-api';
|
|
108
|
+
|
|
109
|
+
@Injectable()
|
|
110
|
+
class UserService {
|
|
111
|
+
async findAll() {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Get('/users')
|
|
117
|
+
export class ListUsers implements RabApiGet<ControllerT> {
|
|
118
|
+
constructor(private userService: UserService) {}
|
|
119
|
+
|
|
120
|
+
handler: ControllerT['request'] = async () => {
|
|
121
|
+
return await this.userService.findAll();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Routing
|
|
127
|
+
|
|
128
|
+
Group related controllers with routers:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
app.route({
|
|
132
|
+
basePath: '/users',
|
|
133
|
+
controllers: [ListUsers, CreateUser, UpdateUser, DeleteUser],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Nested routes
|
|
137
|
+
app.route({
|
|
138
|
+
basePath: '/users',
|
|
139
|
+
controllers: [
|
|
140
|
+
ListUsers,
|
|
141
|
+
RabApi.createRouter({
|
|
142
|
+
basePath: '/:userId/posts',
|
|
143
|
+
controllers: [ListPosts, CreatePost],
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Validation
|
|
150
|
+
|
|
151
|
+
### Request Body
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const createProductSchema = Joi.object({
|
|
155
|
+
name: Joi.string().min(3).required(),
|
|
156
|
+
price: Joi.number().positive().required(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
@Post('/products', { bodySchema: createProductSchema })
|
|
160
|
+
export class CreateProduct implements RabApiPost<ControllerT> {
|
|
161
|
+
handler: ControllerT['request'] = async (request) => {
|
|
162
|
+
const { name, price } = request.body; // validated
|
|
163
|
+
return { id: '1', name, price };
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Query Parameters
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const listSchema = Joi.object({
|
|
172
|
+
page: Joi.number().integer().min(1).default(1),
|
|
173
|
+
limit: Joi.number().integer().min(1).max(100).default(10),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
@Get('/products', { querySchema: listSchema })
|
|
177
|
+
export class ListProducts implements RabApiGet<ControllerT> {
|
|
178
|
+
handler: ControllerT['request'] = async (request) => {
|
|
179
|
+
const { page, limit } = request.query; // validated
|
|
180
|
+
return { items: [], page, limit };
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Authentication
|
|
186
|
+
|
|
187
|
+
### JWT Setup
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const app = RabApi.createApp({
|
|
191
|
+
auth: {
|
|
192
|
+
jwt: {
|
|
193
|
+
secret_key: process.env.JWT_SECRET!,
|
|
194
|
+
algorithms: ['HS256'],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Protected Routes
|
|
201
|
+
|
|
202
|
+
Routes are protected by default. Make a route public:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
@Post('/auth/login', { isProtected: false })
|
|
206
|
+
export class Login implements RabApiPost<ControllerT> {
|
|
207
|
+
// Public endpoint
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Access authenticated user:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
@Get('/profile')
|
|
215
|
+
export class GetProfile implements RabApiGet<ControllerT> {
|
|
216
|
+
handler: ControllerT['request'] = async (request) => {
|
|
217
|
+
const user = request.auth; // JWT payload
|
|
218
|
+
return { userId: user.userId };
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Authorization
|
|
224
|
+
|
|
225
|
+
Use permission-based access control:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
@Post('/admin/users', { permission: 'canCreateUser' })
|
|
229
|
+
export class CreateUser implements RabApiPost<ControllerT> {
|
|
230
|
+
// Only users with 'canCreateUser' permission
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Integrate with `@softin/rab-access`:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { Rab } from '@softin/rab-access';
|
|
238
|
+
|
|
239
|
+
const permissions = Rab.schema({
|
|
240
|
+
canCreateUser: [Rab.grant('admin'), Rab.grant('superAdmin')],
|
|
241
|
+
canDeleteUser: [Rab.grant('superAdmin')],
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Error Handling
|
|
246
|
+
|
|
247
|
+
Built-in exceptions:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import {
|
|
251
|
+
BadRequestException,
|
|
252
|
+
UnauthorizedException,
|
|
253
|
+
ForbiddenException,
|
|
254
|
+
NotFoundException,
|
|
255
|
+
ConflictException,
|
|
256
|
+
} from 'rab-api';
|
|
257
|
+
|
|
258
|
+
@Get('/users/:id')
|
|
259
|
+
export class GetUser implements RabApiGet<ControllerT> {
|
|
260
|
+
handler: ControllerT['request'] = async (request) => {
|
|
261
|
+
const user = await findUser(request.params.id);
|
|
262
|
+
if (!user) throw new NotFoundException('User not found');
|
|
263
|
+
return user;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Custom error handler:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
const app = RabApi.createApp({
|
|
272
|
+
errorHandler: (err, req, res, next) => {
|
|
273
|
+
if (err instanceof RabApiError) {
|
|
274
|
+
return res.status(err.statusCode).json({ error: err.message });
|
|
275
|
+
}
|
|
276
|
+
return res.status(500).json({ error: 'Internal error' });
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Middleware
|
|
282
|
+
|
|
283
|
+
Apply middleware at different levels:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Route level
|
|
287
|
+
@Get('/users', { pipes: [loggerMiddleware] })
|
|
288
|
+
export class ListUsers {}
|
|
289
|
+
|
|
290
|
+
// Router level
|
|
291
|
+
app.route({
|
|
292
|
+
basePath: '/api',
|
|
293
|
+
pipes: [corsMiddleware, loggerMiddleware],
|
|
294
|
+
controllers: [/* ... */],
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Conditional
|
|
298
|
+
const conditionalAuth = (route) => {
|
|
299
|
+
return route.isProtected ? [authMiddleware] : [];
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
app.route({
|
|
303
|
+
pipes: [conditionalAuth],
|
|
304
|
+
controllers: [/* ... */],
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Advanced Features
|
|
309
|
+
|
|
310
|
+
### Controller Type Helpers
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// Type helpers for controllers
|
|
314
|
+
PostController<TBody, TResponse, TParams?, TUser?, TQuery?>
|
|
315
|
+
GetController<TResponse, TQuery?, TParams?, TUser?>
|
|
316
|
+
PutController<TBody, TResponse, TParams?, TUser?, TQuery?>
|
|
317
|
+
PatchController<TBody, TResponse, TParams?, TUser?, TQuery?>
|
|
318
|
+
DeleteController<TParams?, TUser?>
|
|
319
|
+
|
|
320
|
+
// Interface implementations
|
|
321
|
+
RabApiPost<T> | AtomApiPost<T>
|
|
322
|
+
RabApiGet<T> | AtomApiGet<T>
|
|
323
|
+
RabApiPut<T> | AtomApiPut<T>
|
|
324
|
+
RabApiPatch<T> | AtomApiPatch<T>
|
|
325
|
+
RabApiDelete<T> | AtomApiDelete<T>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Route Options
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
interface RouteOptions {
|
|
332
|
+
bodySchema?: Joi.ObjectSchema; // Body validation
|
|
333
|
+
querySchema?: Joi.ObjectSchema; // Query validation
|
|
334
|
+
isProtected?: boolean; // JWT required (default: true)
|
|
335
|
+
permission?: string; // Permission name
|
|
336
|
+
pipes?: Function[]; // Middleware
|
|
337
|
+
excludeFromDocs?: boolean; // Hide from OpenAPI
|
|
338
|
+
tags?: string[]; // OpenAPI tags
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Complete Example
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { Get, Post, Put, Delete, RabApi, Injectable } from 'rab-api';
|
|
346
|
+
import * as Joi from 'joi';
|
|
347
|
+
|
|
348
|
+
// Service
|
|
349
|
+
@Injectable()
|
|
350
|
+
class ProductService {
|
|
351
|
+
async findAll() { return []; }
|
|
352
|
+
async create(data: any) { return { id: '1', ...data }; }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Controllers
|
|
356
|
+
const createSchema = Joi.object({
|
|
357
|
+
name: Joi.string().required(),
|
|
358
|
+
price: Joi.number().required(),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
@Get('/')
|
|
362
|
+
class ListProducts {
|
|
363
|
+
constructor(private service: ProductService) {}
|
|
364
|
+
handler = async () => await this.service.findAll();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
@Post('/', { bodySchema: createSchema })
|
|
368
|
+
class CreateProduct {
|
|
369
|
+
constructor(private service: ProductService) {}
|
|
370
|
+
handler = async (req) => await this.service.create(req.body);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// App
|
|
374
|
+
const app = RabApi.createApp({
|
|
375
|
+
auth: { jwt: { secret_key: 'secret', algorithms: ['HS256'] } },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
app.use(express.json());
|
|
379
|
+
app.route({
|
|
380
|
+
basePath: '/products',
|
|
381
|
+
controllers: [ListProducts, CreateProduct],
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
app.listen(3000);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT © Softin Hub
|
|
390
|
+
|
|
391
|
+
## Support
|
|
392
|
+
|
|
393
|
+
Email: softin.developer@gmail.com
|
package/index.cjs.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|