lieko-express 0.0.1 → 0.0.3
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 +1854 -0
- package/lieko-express.js +4 -1
- package/package.json +7 -2
package/README.md
ADDED
|
@@ -0,0 +1,1854 @@
|
|
|
1
|
+
# **Lieko-express — A Modern, Minimal, REST API Framework for Node.js**
|
|
2
|
+
|
|
3
|
+
A lightweight, fast, and modern Node.js REST API framework built on top of the native `http` module. Zero external dependencies for core functionality.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](https://www.npmjs.com/package/lieko-express)
|
|
14
|
+
|
|
15
|
+
[](https://github.com/eiwSrvt/lieko-express)
|
|
16
|
+
[](https://discord.gg/EpgPqjvd)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
| Metric | Express.js | Lieko Express | Improvement |
|
|
21
|
+
|--------|------------|---------------|-------------|
|
|
22
|
+
| **Max Throughput** | 13,303 req/sec | **16,644 req/sec** | **+25.1%** |
|
|
23
|
+
| **Best Case (POST)** | 11,202 req/sec | **16,644 req/sec** | **+48.6%** |
|
|
24
|
+
| **Worst Case (GET)** | 13,303 req/sec | **16,152 req/sec** | **+21.4%** |
|
|
25
|
+
| **Average Latency** | 30µs | **10µs** | **-66.7%** |
|
|
26
|
+
| **Total Requests** | 336k (POST) | **499k (POST)** | **+48.5%** |
|
|
27
|
+
|
|
28
|
+
**Key Takeaways:**
|
|
29
|
+
- ✅ **Up to 49% higher throughput** than Express.js
|
|
30
|
+
- ✅ **67% lower latency** (10µs vs 30µs)
|
|
31
|
+
- ✅ **Consistently outperforms** Express in all tests
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- 🎯 **Zero Dependencies** - Built entirely on Node.js native modules
|
|
36
|
+
- ⚡ **High Performance** - Optimized for speed with minimal overhead
|
|
37
|
+
- 🛡️ **Built-in Validation** - Comprehensive validation system with 15+ validators
|
|
38
|
+
- 🔄 **Router Support** - Modular routing with nested routers
|
|
39
|
+
- 🔐 **Middleware System** - Global and route-specific middlewares
|
|
40
|
+
- 📝 **Body Parsing** - JSON, URL-encoded, and multipart/form-data support
|
|
41
|
+
- 🎨 **Response Helpers** - Convenient methods for common responses
|
|
42
|
+
- 🔍 **Route Parameters** - Dynamic route matching with named parameters
|
|
43
|
+
- 🌐 **Query Parsing** - Automatic type conversion for query parameters
|
|
44
|
+
- 📤 **File Upload** - Built-in multipart/form-data handling
|
|
45
|
+
- ⚠️ **Error Handling** - Structured error responses with status codes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## 📚 Table of Contents
|
|
49
|
+
|
|
50
|
+
- [Philosophy](#philosophy)
|
|
51
|
+
- [Basic Usage](#basic-usage)
|
|
52
|
+
- [Routing](#routing)
|
|
53
|
+
- [Middlewares](#middlewares)
|
|
54
|
+
- [Request & Response](#request--response)
|
|
55
|
+
- [Validation](#validation)
|
|
56
|
+
- [Routers](#routers)
|
|
57
|
+
- [Error Handling](#error-handling)
|
|
58
|
+
- [Advanced Examples](#advanced-examples)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
# 🧠 Philosophy
|
|
63
|
+
|
|
64
|
+
Lieko attempts to solve several common issues with classic Node frameworks:
|
|
65
|
+
|
|
66
|
+
### ❌ Express problems:
|
|
67
|
+
|
|
68
|
+
* No route grouping
|
|
69
|
+
* Weak middlewares
|
|
70
|
+
* No validation
|
|
71
|
+
* Messy routers
|
|
72
|
+
* Confusing trust proxy behavior
|
|
73
|
+
* No helpers
|
|
74
|
+
|
|
75
|
+
### ✔ Lieko solutions:
|
|
76
|
+
|
|
77
|
+
* Clean **group-based routing**
|
|
78
|
+
* Built-in validators
|
|
79
|
+
* Typed & normalized request fields
|
|
80
|
+
* Robust error system
|
|
81
|
+
* First-class JSON & uploads
|
|
82
|
+
* Simple, powerful helpers (res.ok, res.error…)
|
|
83
|
+
* Predictable IP handling
|
|
84
|
+
|
|
85
|
+
Lieko feels familiar but is dramatically more coherent and modern.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## 📦 Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install lieko-express
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 🚀 Quick Start
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const Lieko = require('lieko-express');
|
|
99
|
+
|
|
100
|
+
const app = Lieko();
|
|
101
|
+
|
|
102
|
+
app.get('/', (req, res) => {
|
|
103
|
+
res.json({ message: 'Hello, Lieko!' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.listen(3000, () => {
|
|
107
|
+
console.log('Server running on port 3000');
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 🎯 Basic Usage
|
|
112
|
+
|
|
113
|
+
### Creating an Application
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const Lieko = require('lieko-express');
|
|
117
|
+
const app = Lieko();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### HTTP Methods
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// GET request
|
|
124
|
+
app.get('/users', (req, res) => {
|
|
125
|
+
res.json({ users: [] });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// POST request
|
|
129
|
+
app.post('/users', (req, res) => {
|
|
130
|
+
const user = req.body;
|
|
131
|
+
res.status(201).json({ user });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// PUT request
|
|
135
|
+
app.put('/users/:id', (req, res) => {
|
|
136
|
+
const { id } = req.params;
|
|
137
|
+
res.json({ updated: true, id });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// DELETE request
|
|
141
|
+
app.delete('/users/:id', (req, res) => {
|
|
142
|
+
res.json({ deleted: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// PATCH request
|
|
146
|
+
app.patch('/users/:id', (req, res) => {
|
|
147
|
+
res.json({ patched: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ALL methods
|
|
151
|
+
app.all('/admin', (req, res) => {
|
|
152
|
+
res.json({ method: req.method });
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# 🛣 Routing
|
|
158
|
+
|
|
159
|
+
Lieko provides classic `.get()`, `.post()`, `.patch()`, `.delete()`:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
app.get('/hello', (req, res) => res.ok('Hello!'));
|
|
163
|
+
app.post('/upload', uploadFile);
|
|
164
|
+
app.patch('/users/:id', updateUser);
|
|
165
|
+
app.delete('/posts/:id', deletePost);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Features
|
|
169
|
+
|
|
170
|
+
✔ Params automatically extracted
|
|
171
|
+
|
|
172
|
+
✔ Query auto-typed
|
|
173
|
+
|
|
174
|
+
✔ Body parsed and typed
|
|
175
|
+
|
|
176
|
+
✔ Wildcards available
|
|
177
|
+
|
|
178
|
+
✔ Trailing slashes handled intelligently
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
# 🧩 Route Groups
|
|
183
|
+
|
|
184
|
+
Lieko supports Laravel-style **group routing** with middleware inheritance:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
// (api) = name of subroutes
|
|
188
|
+
app.group('/api', auth, (api) => {
|
|
189
|
+
|
|
190
|
+
api.get('/profile', (req, res) =>
|
|
191
|
+
res.ok({ user: req.user })
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
api.group('/admin', requireAdmin, (admin) => {
|
|
195
|
+
|
|
196
|
+
admin.get('/stats', (req, res) =>
|
|
197
|
+
res.ok({ admin: true })
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Group behavior
|
|
206
|
+
|
|
207
|
+
* Middlewares inherited by all children
|
|
208
|
+
* Prefix applied recursively
|
|
209
|
+
* You can nest indefinitely
|
|
210
|
+
* Works inside routers too
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 📦 Nested Routers
|
|
215
|
+
|
|
216
|
+
Routers are fully nestable:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
const { Router } = require('lieko-express');
|
|
220
|
+
|
|
221
|
+
const app = Router();
|
|
222
|
+
|
|
223
|
+
app.get('/', listUsers);
|
|
224
|
+
app.post('/', createUser);
|
|
225
|
+
app.get('/:id', getUser);
|
|
226
|
+
|
|
227
|
+
app.group('/api', auth, (api) => {
|
|
228
|
+
api.group('/admin', requireAdmin, (admin) => {
|
|
229
|
+
admin.use('/users', users);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Group without TOP middleware
|
|
234
|
+
app.group('/api', (api) => {
|
|
235
|
+
api.group('/admin', requireAdmin, (admin) => {
|
|
236
|
+
admin.use('/users', users);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
app.group('/admin2', auth, requireAdmin, rateLimit, (admin) => {
|
|
241
|
+
admin.get('/stats', getStats);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
api.group(
|
|
245
|
+
'/secure',
|
|
246
|
+
requireAuth(config),
|
|
247
|
+
requirePermissions(['users.read', 'posts.write']),
|
|
248
|
+
requireAdmin('super'),
|
|
249
|
+
(secure) => {
|
|
250
|
+
secure.get('/panel', ...);
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
✔ Router inherits middleware from its parent groups
|
|
255
|
+
|
|
256
|
+
✔ Paths automatically expanded
|
|
257
|
+
|
|
258
|
+
✔ Perfect for modular architecture
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# 🧩 API Versioning
|
|
262
|
+
|
|
263
|
+
With groups, versioning becomes trivial:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
app.group('/api/v1', (v1) => {
|
|
267
|
+
v1.get('/users', handlerV1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
app.group('/api/v2', (v2) => {
|
|
271
|
+
v2.get('/users', handlerV2);
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
### Query Parameters
|
|
277
|
+
|
|
278
|
+
Query parameters are automatically parsed and converted to appropriate types:
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
app.get('/search', (req, res) => {
|
|
282
|
+
// GET /search?q=hello&page=2&active=true&price=19.99
|
|
283
|
+
|
|
284
|
+
console.log(req.query);
|
|
285
|
+
// {
|
|
286
|
+
// q: 'hello', // string
|
|
287
|
+
// page: 2, // number (auto-converted)
|
|
288
|
+
// active: true, // boolean (auto-converted)
|
|
289
|
+
// price: 19.99 // float (auto-converted)
|
|
290
|
+
// }
|
|
291
|
+
|
|
292
|
+
res.json({ query: req.query });
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Automatic Type Conversion:**
|
|
297
|
+
- `"123"` → `123` (number)
|
|
298
|
+
- `"19.99"` → `19.99` (float)
|
|
299
|
+
- `"true"` → `true` (boolean)
|
|
300
|
+
- `"false"` → `false` (boolean)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
## 🔧 Middlewares
|
|
304
|
+
|
|
305
|
+
### Global Middlewares
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Logger middleware
|
|
309
|
+
app.use((req, res, next) => {
|
|
310
|
+
console.log(`${req.method} ${req.url}`);
|
|
311
|
+
next();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// CORS middleware
|
|
315
|
+
app.use((req, res, next) => {
|
|
316
|
+
res.set('Access-Control-Allow-Origin', '*');
|
|
317
|
+
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
318
|
+
next();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Auth middleware
|
|
322
|
+
const authMiddleware = (req, res, next) => {
|
|
323
|
+
const token = req.headers['authorization'];
|
|
324
|
+
|
|
325
|
+
if (!token) {
|
|
326
|
+
return res.error({
|
|
327
|
+
code: 'NO_TOKEN_PROVIDED',
|
|
328
|
+
message: 'Authorization required'
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
req.user = { id: 1, role: 'admin' };
|
|
333
|
+
next();
|
|
334
|
+
};
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Path-Specific Middlewares
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
// Apply to specific path
|
|
341
|
+
app.use('/api', (req, res, next) => {
|
|
342
|
+
console.log('API route accessed');
|
|
343
|
+
next();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Multiple middlewares
|
|
347
|
+
app.use('/admin', authMiddleware, checkRole, (req, res, next) => {
|
|
348
|
+
next();
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Route-Level Middlewares
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
// Single middleware
|
|
356
|
+
app.get('/protected', authMiddleware, (req, res) => {
|
|
357
|
+
res.json({ user: req.user });
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Multiple middlewares
|
|
361
|
+
app.post('/admin/users',
|
|
362
|
+
authMiddleware,
|
|
363
|
+
checkRole('admin'),
|
|
364
|
+
validate(userSchema),
|
|
365
|
+
(req, res) => {
|
|
366
|
+
res.json({ created: true });
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Async Middlewares
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
// Async/await support
|
|
375
|
+
app.use(async (req, res, next) => {
|
|
376
|
+
try {
|
|
377
|
+
req.data = await fetchDataFromDB();
|
|
378
|
+
next();
|
|
379
|
+
} catch (error) {
|
|
380
|
+
next(error);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# 🔍 Request Object
|
|
387
|
+
|
|
388
|
+
Lieko’s `req` object provides:
|
|
389
|
+
|
|
390
|
+
### General
|
|
391
|
+
|
|
392
|
+
| Field | Description |
|
|
393
|
+
| ----------------- | ----------------- |
|
|
394
|
+
| `req.method` | HTTP method |
|
|
395
|
+
| `req.path` | Without query |
|
|
396
|
+
| `req.originalUrl` | Full original URL |
|
|
397
|
+
| `req.protocol` | `http` or `https` |
|
|
398
|
+
| `req.secure` | Boolean |
|
|
399
|
+
| `req.hostname` | Hostname |
|
|
400
|
+
| `req.subdomains` | Array |
|
|
401
|
+
| `req.xhr` | AJAX detection |
|
|
402
|
+
| `req.params` | Route parameters |
|
|
403
|
+
| `req.query` | Auto-typed query |
|
|
404
|
+
| `req.body` | Parsed body |
|
|
405
|
+
| `req.files` | Uploaded files |
|
|
406
|
+
|
|
407
|
+
### IP Handling
|
|
408
|
+
|
|
409
|
+
| Field | Description |
|
|
410
|
+
| ------------- | ---------------------- |
|
|
411
|
+
| `req.ip.raw` | Original IP |
|
|
412
|
+
| `req.ip.ipv4` | IPv4 (auto normalized) |
|
|
413
|
+
| `req.ip.ipv6` | IPv6 |
|
|
414
|
+
| `req.ips` | Proxy chain |
|
|
415
|
+
|
|
416
|
+
Lieko safely handles:
|
|
417
|
+
|
|
418
|
+
* IPv4
|
|
419
|
+
* IPv6
|
|
420
|
+
* IPv4-mapped IPv6
|
|
421
|
+
* Multiple proxies
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# 🎯 Response Object (`res`)
|
|
425
|
+
|
|
426
|
+
Lieko enhances Node's native response object with convenient helper methods.
|
|
427
|
+
|
|
428
|
+
### **General Response Methods**
|
|
429
|
+
|
|
430
|
+
| Method / Field | Description |
|
|
431
|
+
| -------------------------- | ---------------------------------------- |
|
|
432
|
+
| `res.status(code)` | Sets HTTP status code (chainable). |
|
|
433
|
+
| `res.setHeader(name, val)` | Sets a response header. |
|
|
434
|
+
| `res.getHeader(name)` | Gets a response header. |
|
|
435
|
+
| `res.removeHeader(name)` | Removes a header. |
|
|
436
|
+
| `res.type(mime)` | Sets `Content-Type` automatically. |
|
|
437
|
+
| `res.send(data)` | Sends raw data (string, buffer, object). |
|
|
438
|
+
| `res.json(obj)` | Sends JSON with correct headers. |
|
|
439
|
+
| `res.end(body)` | Ends the response manually. |
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ✅ **High-Level Helpers**
|
|
443
|
+
|
|
444
|
+
### **Success Helpers**
|
|
445
|
+
|
|
446
|
+
| Method | Description |
|
|
447
|
+
| ------------------------ | -------------------------------------------------- |
|
|
448
|
+
| `res.ok(data)` | Sends `{ success: true, data }` with status `200`. |
|
|
449
|
+
| `res.created(data)` | Sends `{ success: true, data }` with status `201`. |
|
|
450
|
+
| `res.noContent()` | Sends status `204` with no body. |
|
|
451
|
+
| `res.paginated(payload)` | Standard API pagination output. |
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ❌ **Error Helpers**
|
|
455
|
+
|
|
456
|
+
| Method | Description |
|
|
457
|
+
| ----------------------- | ------------------------------------------------- |
|
|
458
|
+
| `res.error(codeOrObj)` | Sends a normalized JSON error response (400–500). |
|
|
459
|
+
| `res.badRequest(msg)` | Sends `400 Bad Request`. |
|
|
460
|
+
| `res.unauthorized(msg)` | Sends `401 Unauthorized`. |
|
|
461
|
+
| `res.forbidden(msg)` | Sends `403 Forbidden`. |
|
|
462
|
+
| `res.notFound(msg)` | Sends `404 Not Found`. |
|
|
463
|
+
| `res.serverError(msg)` | Sends `500 Server Error`. |
|
|
464
|
+
|
|
465
|
+
> All these helpers output a consistent JSON error structure:
|
|
466
|
+
>
|
|
467
|
+
> ```json
|
|
468
|
+
> { "success": false, "error": { "code": "...", "message": "..." } }
|
|
469
|
+
> ```
|
|
470
|
+
|
|
471
|
+
# 🧠 **Content-Type Helpers**
|
|
472
|
+
|
|
473
|
+
### **HTML**
|
|
474
|
+
|
|
475
|
+
| Method | Description |
|
|
476
|
+
| ----------------------------- | ---------------------------------------------------------- |
|
|
477
|
+
| `res.html(html, status?)` | Short alias for `sendHTML()`. | |
|
|
478
|
+
|
|
479
|
+
### **Redirects**
|
|
480
|
+
|
|
481
|
+
| Method | Description |
|
|
482
|
+
| -------------------------- | ------------------------------------- |
|
|
483
|
+
| `res.redirect(url, code?)` | Redirects the client (default `302`). |
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# 📦 **Low-Level Output Controls**
|
|
487
|
+
|
|
488
|
+
| Method | Description |
|
|
489
|
+
| ---------------------------------- | ----------------------------------------------- |
|
|
490
|
+
| `res.write(chunk)` | Writes a raw chunk without ending the response. |
|
|
491
|
+
| `res.flushHeaders()` | Sends the headers immediately. |
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
### JSON helpers
|
|
495
|
+
|
|
496
|
+
```js
|
|
497
|
+
res.ok(data);
|
|
498
|
+
res.created(data);
|
|
499
|
+
res.accepted(data);
|
|
500
|
+
res.noContent();
|
|
501
|
+
res.error({ code: "INVALID_DATA" });
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Pagination helper
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
res.paginated(items, page, limit, total);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Response formatting
|
|
511
|
+
|
|
512
|
+
* Uniform JSON structure
|
|
513
|
+
* Automatic status codes
|
|
514
|
+
* Error code → HTTP mapping
|
|
515
|
+
* String errors also supported (`res.error("Invalid user")`)
|
|
516
|
+
|
|
517
|
+
# 📦 Body Parsing
|
|
518
|
+
|
|
519
|
+
Lieko supports:
|
|
520
|
+
|
|
521
|
+
### ✔ JSON
|
|
522
|
+
|
|
523
|
+
### ✔ URL-encoded
|
|
524
|
+
|
|
525
|
+
### ✔ Multipart form-data (files)
|
|
526
|
+
|
|
527
|
+
Uploads end up in:
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
req.files = {
|
|
531
|
+
avatar: {
|
|
532
|
+
filename: "...",
|
|
533
|
+
mimetype: "...",
|
|
534
|
+
size: 1234,
|
|
535
|
+
buffer: <Buffer>
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Query & body fields are **auto converted**:
|
|
541
|
+
|
|
542
|
+
```
|
|
543
|
+
"123" → 123
|
|
544
|
+
"true" → true
|
|
545
|
+
"false" → false
|
|
546
|
+
"null" → null
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Response Methods
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
// JSON response
|
|
553
|
+
res.json({ message: 'Hello' });
|
|
554
|
+
|
|
555
|
+
// Send any data
|
|
556
|
+
res.send('Plain text');
|
|
557
|
+
res.send({ object: 'data' });
|
|
558
|
+
|
|
559
|
+
// Status code
|
|
560
|
+
res.status(404).json({ error: 'Not found' });
|
|
561
|
+
|
|
562
|
+
// Set headers
|
|
563
|
+
res.set('X-Custom-Header', 'value');
|
|
564
|
+
res.header('Content-Type', 'text/html');
|
|
565
|
+
|
|
566
|
+
// Redirect
|
|
567
|
+
res.redirect('/new-url');
|
|
568
|
+
|
|
569
|
+
// Success response helper
|
|
570
|
+
res.ok({ user: userData }, 'User retrieved successfully');
|
|
571
|
+
// Returns: { success: true, data: userData, message: '...' }
|
|
572
|
+
|
|
573
|
+
// Error response helper
|
|
574
|
+
res.error({
|
|
575
|
+
code: 'NOT_FOUND',
|
|
576
|
+
message: 'Resource not found'
|
|
577
|
+
});
|
|
578
|
+
// Returns: { success: false, error: { code, message } }
|
|
579
|
+
|
|
580
|
+
// Response locals (shared data)
|
|
581
|
+
res.locals.user = currentUser;
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Body Parsing
|
|
585
|
+
|
|
586
|
+
**Automatic parsing for:**
|
|
587
|
+
|
|
588
|
+
1. **JSON** (`application/json`)
|
|
589
|
+
```javascript
|
|
590
|
+
app.post('/api/data', (req, res) => {
|
|
591
|
+
console.log(req.body); // { name: 'John', age: 30 }
|
|
592
|
+
res.json({ received: true });
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
2. **URL-encoded** (`application/x-www-form-urlencoded`)
|
|
597
|
+
```javascript
|
|
598
|
+
app.post('/form', (req, res) => {
|
|
599
|
+
console.log(req.body); // { username: 'john', password: '***' }
|
|
600
|
+
res.json({ ok: true });
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
3. **Multipart/form-data** (file uploads)
|
|
605
|
+
```javascript
|
|
606
|
+
app.post('/upload', (req, res) => {
|
|
607
|
+
const file = req.files.avatar;
|
|
608
|
+
|
|
609
|
+
console.log(file.filename); // 'profile.jpg'
|
|
610
|
+
console.log(file.contentType); // 'image/jpeg'
|
|
611
|
+
console.log(file.data); // Buffer
|
|
612
|
+
console.log(file.data.length); // File size in bytes
|
|
613
|
+
|
|
614
|
+
// Other form fields
|
|
615
|
+
console.log(req.body.username); // 'john'
|
|
616
|
+
|
|
617
|
+
res.json({ uploaded: true });
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## ✅ Validation
|
|
622
|
+
|
|
623
|
+
### Built-in Validators
|
|
624
|
+
|
|
625
|
+
```javascript
|
|
626
|
+
const { schema, validators, validate } = require('lieko-express');
|
|
627
|
+
|
|
628
|
+
const userSchema = schema({
|
|
629
|
+
// Required field
|
|
630
|
+
username: [
|
|
631
|
+
validators.required('Username is required'),
|
|
632
|
+
validators.string(),
|
|
633
|
+
validators.minLength(3),
|
|
634
|
+
validators.maxLength(20),
|
|
635
|
+
validators.pattern(/^[a-zA-Z0-9_]+$/, 'Alphanumeric only')
|
|
636
|
+
],
|
|
637
|
+
|
|
638
|
+
// Email validation
|
|
639
|
+
email: [
|
|
640
|
+
validators.required(),
|
|
641
|
+
validators.email('Invalid email format')
|
|
642
|
+
],
|
|
643
|
+
|
|
644
|
+
// Number validation
|
|
645
|
+
age: [
|
|
646
|
+
validators.required(),
|
|
647
|
+
validators.number(),
|
|
648
|
+
validators.min(18, 'Must be 18 or older'),
|
|
649
|
+
validators.max(120)
|
|
650
|
+
],
|
|
651
|
+
|
|
652
|
+
// Boolean validation
|
|
653
|
+
active: [
|
|
654
|
+
validators.boolean()
|
|
655
|
+
],
|
|
656
|
+
|
|
657
|
+
// Must be true (for terms acceptance)
|
|
658
|
+
acceptTerms: [
|
|
659
|
+
validators.required(),
|
|
660
|
+
validators.mustBeTrue('You must accept terms')
|
|
661
|
+
],
|
|
662
|
+
|
|
663
|
+
// Enum validation
|
|
664
|
+
role: [
|
|
665
|
+
validators.oneOf(['user', 'admin', 'moderator'])
|
|
666
|
+
],
|
|
667
|
+
|
|
668
|
+
// Custom validator
|
|
669
|
+
password: [
|
|
670
|
+
validators.required(),
|
|
671
|
+
validators.custom((value) => {
|
|
672
|
+
return value.length >= 8 && /[A-Z]/.test(value);
|
|
673
|
+
}, 'Password must be 8+ chars with uppercase')
|
|
674
|
+
],
|
|
675
|
+
|
|
676
|
+
// Equal to another value
|
|
677
|
+
confirmPassword: [
|
|
678
|
+
validators.equal(req => req.body.password, 'Passwords must match')
|
|
679
|
+
]
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Use in route
|
|
683
|
+
app.post('/register', validate(userSchema), (req, res) => {
|
|
684
|
+
// If validation passes, this runs
|
|
685
|
+
res.status(201).json({ success: true });
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### All Available Validators
|
|
690
|
+
|
|
691
|
+
| Validator | Description | Example |
|
|
692
|
+
| ------------------------------ | --------------------------------------------------- | ---------------------------------------- |
|
|
693
|
+
| `required(message?)` | Field must be present (not null/empty) | `validators.required()` |
|
|
694
|
+
| `requiredTrue(message?)` | Must be true (accepts `true`, `"true"`, `1`, `"1"`) | `validators.requiredTrue()` |
|
|
695
|
+
| `optional()` | Skip validation if field is missing | `validators.optional()` |
|
|
696
|
+
| `string(message?)` | Must be a string | `validators.string()` |
|
|
697
|
+
| `number(message?)` | Must be a number (no coercion) | `validators.number()` |
|
|
698
|
+
| `boolean(message?)` | Must be boolean-like (`true/false`, `"1"/"0"`) | `validators.boolean()` |
|
|
699
|
+
| `integer(message?)` | Must be an integer | `validators.integer()` |
|
|
700
|
+
| `positive(message?)` | Must be > 0 | `validators.positive()` |
|
|
701
|
+
| `negative(message?)` | Must be < 0 | `validators.negative()` |
|
|
702
|
+
| `email(message?)` | Must be a valid email | `validators.email()` |
|
|
703
|
+
| `min(value, message?)` | Minimum number or string length | `validators.min(3)` |
|
|
704
|
+
| `max(value, message?)` | Maximum number or string length | `validators.max(10)` |
|
|
705
|
+
| `length(n, message?)` | Exact string length | `validators.length(6)` |
|
|
706
|
+
| `minLength(n, message?)` | Minimum string length | `validators.minLength(3)` |
|
|
707
|
+
| `maxLength(n, message?)` | Maximum string length | `validators.maxLength(255)` |
|
|
708
|
+
| `pattern(regex, message?)` | Must match regex | `validators.pattern(/^\d+$/)` |
|
|
709
|
+
| `startsWith(prefix, message?)` | Must start with prefix | `validators.startsWith("abc")` |
|
|
710
|
+
| `endsWith(suffix, message?)` | Must end with suffix | `validators.endsWith(".jpg")` |
|
|
711
|
+
| `oneOf(values, message?)` | Value must be in list | `validators.oneOf(['admin','user'])` |
|
|
712
|
+
| `notOneOf(values, message?)` | Value cannot be in list | `validators.notOneOf(['root','system'])` |
|
|
713
|
+
| `custom(fn, message?)` | Custom validation | `validators.custom(val => val > 0)` |
|
|
714
|
+
| `equal(value, message?)` | Must equal specific value | `validators.equal("yes")` |
|
|
715
|
+
| `mustBeTrue(message?)` | Must be true (alias of requiredTrue) | `validators.mustBeTrue()` |
|
|
716
|
+
| `mustBeFalse(message?)` | Must be false | `validators.mustBeFalse()` |
|
|
717
|
+
| `date(message?)` | Must be valid date | `validators.date()` |
|
|
718
|
+
| `before(date, message?)` | Must be < given date | `validators.before("2025-01-01")` |
|
|
719
|
+
| `after(date, message?)` | Must be > given date | `validators.after("2020-01-01")` |
|
|
720
|
+
|
|
721
|
+
## **Basic Schema Example**
|
|
722
|
+
|
|
723
|
+
```js
|
|
724
|
+
const userSchema = schema({
|
|
725
|
+
name: [
|
|
726
|
+
validators.required(),
|
|
727
|
+
validators.string(),
|
|
728
|
+
validators.minLength(3),
|
|
729
|
+
],
|
|
730
|
+
age: [
|
|
731
|
+
validators.required(),
|
|
732
|
+
validators.number(),
|
|
733
|
+
validators.min(18),
|
|
734
|
+
],
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## **Boolean Example**
|
|
739
|
+
|
|
740
|
+
```js
|
|
741
|
+
const schema = schema({
|
|
742
|
+
subscribed: [
|
|
743
|
+
validators.boolean()
|
|
744
|
+
]
|
|
745
|
+
});
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Accepted:
|
|
749
|
+
|
|
750
|
+
```
|
|
751
|
+
true, false, "true", "false", 1, 0, "1", "0"
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
## **Email Example**
|
|
755
|
+
|
|
756
|
+
```js
|
|
757
|
+
const schema = schema({
|
|
758
|
+
email: [
|
|
759
|
+
validators.required(),
|
|
760
|
+
validators.email()
|
|
761
|
+
]
|
|
762
|
+
});
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
## **Username with rules**
|
|
766
|
+
|
|
767
|
+
```js
|
|
768
|
+
const usernameSchema = schema({
|
|
769
|
+
username: [
|
|
770
|
+
validators.required(),
|
|
771
|
+
validators.string(),
|
|
772
|
+
validators.minLength(3),
|
|
773
|
+
validators.maxLength(16),
|
|
774
|
+
validators.pattern(/^[a-zA-Z0-9_]+$/, "Invalid username")
|
|
775
|
+
]
|
|
776
|
+
});
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## **Password strength**
|
|
780
|
+
|
|
781
|
+
```js
|
|
782
|
+
const passwordSchema = schema({
|
|
783
|
+
password: [
|
|
784
|
+
validators.required(),
|
|
785
|
+
validators.minLength(8),
|
|
786
|
+
validators.custom((value) => {
|
|
787
|
+
return /[A-Z]/.test(value) &&
|
|
788
|
+
/[a-z]/.test(value) &&
|
|
789
|
+
/\d/.test(value) &&
|
|
790
|
+
/[!@#$%^&*]/.test(value);
|
|
791
|
+
}, "Weak password")
|
|
792
|
+
]
|
|
793
|
+
});
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## **Multiple-choice fields**
|
|
797
|
+
|
|
798
|
+
```js
|
|
799
|
+
const roleSchema = schema({
|
|
800
|
+
role: [
|
|
801
|
+
validators.oneOf(["admin", "user", "guest"])
|
|
802
|
+
]
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
## **Blacklist Example**
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
const schema = schema({
|
|
811
|
+
username: [
|
|
812
|
+
validators.notOneOf(["root", "admin", "system"])
|
|
813
|
+
]
|
|
814
|
+
});
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
## **Starts / Ends With**
|
|
818
|
+
|
|
819
|
+
```js
|
|
820
|
+
const schema = schema({
|
|
821
|
+
fileName: [
|
|
822
|
+
validators.endsWith(".jpg", "Must be a JPG image")
|
|
823
|
+
]
|
|
824
|
+
});
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
# **Advanced Validation Examples**
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
## **Cross-field validation (matching passwords)**
|
|
832
|
+
|
|
833
|
+
```js
|
|
834
|
+
const registerSchema = schema({
|
|
835
|
+
password: [
|
|
836
|
+
validators.required(),
|
|
837
|
+
validators.minLength(8)
|
|
838
|
+
],
|
|
839
|
+
confirmPassword: [
|
|
840
|
+
validators.required(),
|
|
841
|
+
validators.custom((value, data) => value === data.password, "Passwords do not match")
|
|
842
|
+
]
|
|
843
|
+
});
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
## Conditional Validation (depends on another field)
|
|
848
|
+
|
|
849
|
+
```js
|
|
850
|
+
const orderSchema = schema({
|
|
851
|
+
shippingMethod: [validators.oneOf(["pickup", "delivery"])],
|
|
852
|
+
address: [
|
|
853
|
+
validators.custom((value, data) => {
|
|
854
|
+
if (data.shippingMethod === "delivery")
|
|
855
|
+
return value && value.length > 0;
|
|
856
|
+
return true;
|
|
857
|
+
}, "Address required when using delivery")
|
|
858
|
+
]
|
|
859
|
+
});
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
## Dynamic rules EX: `age` required only if user is not admin
|
|
864
|
+
|
|
865
|
+
```js
|
|
866
|
+
const schema = schema({
|
|
867
|
+
role: [validators.oneOf(["user", "admin"])],
|
|
868
|
+
age: [
|
|
869
|
+
validators.custom((age, data) => {
|
|
870
|
+
if (data.role === "user") return age >= 18;
|
|
871
|
+
return true;
|
|
872
|
+
}, "Users must be 18+")
|
|
873
|
+
]
|
|
874
|
+
});
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
## **Date validation**
|
|
879
|
+
|
|
880
|
+
```js
|
|
881
|
+
const eventSchema = schema({
|
|
882
|
+
startDate: [validators.date()],
|
|
883
|
+
endDate: [
|
|
884
|
+
validators.date(),
|
|
885
|
+
validators.custom((value, data) => {
|
|
886
|
+
return new Date(value) > new Date(data.startDate);
|
|
887
|
+
}, "End date must be after start date")
|
|
888
|
+
]
|
|
889
|
+
});
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
# **Combining many validators**
|
|
894
|
+
|
|
895
|
+
```js
|
|
896
|
+
const schema = schema({
|
|
897
|
+
code: [
|
|
898
|
+
validators.required(),
|
|
899
|
+
validators.string(),
|
|
900
|
+
validators.length(6),
|
|
901
|
+
validators.pattern(/^[A-Z0-9]+$/),
|
|
902
|
+
validators.notOneOf(["AAAAAA", "000000"]),
|
|
903
|
+
]
|
|
904
|
+
});
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
# **Optional field + rules if provided**
|
|
909
|
+
|
|
910
|
+
```js
|
|
911
|
+
const schema = schema({
|
|
912
|
+
nickname: [
|
|
913
|
+
validators.optional(),
|
|
914
|
+
validators.minLength(3),
|
|
915
|
+
validators.maxLength(20)
|
|
916
|
+
]
|
|
917
|
+
});
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
If nickname is empty → no validation.
|
|
921
|
+
If present → must follow rules.
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
# **Example of validation error response**
|
|
925
|
+
|
|
926
|
+
Already provided but I format it more "realistic":
|
|
927
|
+
|
|
928
|
+
```json
|
|
929
|
+
{
|
|
930
|
+
"success": false,
|
|
931
|
+
"message": "Validation failed",
|
|
932
|
+
"errors": [
|
|
933
|
+
{
|
|
934
|
+
"field": "email",
|
|
935
|
+
"message": "Invalid email format",
|
|
936
|
+
"type": "email"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"field": "age",
|
|
940
|
+
"message": "Field must be at least 18",
|
|
941
|
+
"type": "min"
|
|
942
|
+
}
|
|
943
|
+
]
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
### Custom Validation Examples
|
|
949
|
+
|
|
950
|
+
```javascript
|
|
951
|
+
// Password strength
|
|
952
|
+
const passwordSchema = schema({
|
|
953
|
+
password: [
|
|
954
|
+
validators.required(),
|
|
955
|
+
validators.custom((value) => {
|
|
956
|
+
const hasUpperCase = /[A-Z]/.test(value);
|
|
957
|
+
const hasLowerCase = /[a-z]/.test(value);
|
|
958
|
+
const hasNumbers = /\d/.test(value);
|
|
959
|
+
const hasSpecialChar = /[!@#$%^&*]/.test(value);
|
|
960
|
+
|
|
961
|
+
return hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar;
|
|
962
|
+
}, 'Password must contain uppercase, lowercase, number and special char')
|
|
963
|
+
]
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Cross-field validation
|
|
967
|
+
const registrationSchema = schema({
|
|
968
|
+
password: [validators.required(), validators.minLength(8)],
|
|
969
|
+
confirmPassword: [
|
|
970
|
+
validators.required(),
|
|
971
|
+
validators.custom((value, data) => {
|
|
972
|
+
return value === data.password;
|
|
973
|
+
}, 'Passwords do not match')
|
|
974
|
+
]
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// Conditional validation
|
|
978
|
+
const orderSchema = schema({
|
|
979
|
+
shippingAddress: [
|
|
980
|
+
validators.custom((value, data) => {
|
|
981
|
+
// Only required if shipping method is 'delivery'
|
|
982
|
+
if (data.shippingMethod === 'delivery') {
|
|
983
|
+
return value && value.length > 0;
|
|
984
|
+
}
|
|
985
|
+
return true;
|
|
986
|
+
}, 'Shipping address required for delivery')
|
|
987
|
+
]
|
|
988
|
+
});
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
## 🗂️ Routers
|
|
993
|
+
|
|
994
|
+
Routers allow you to create modular, mountable route handlers.
|
|
995
|
+
|
|
996
|
+
### Creating a Router
|
|
997
|
+
|
|
998
|
+
```javascript
|
|
999
|
+
const Lieko = require('lieko-express');
|
|
1000
|
+
const usersRouter = Lieko.Router();
|
|
1001
|
+
|
|
1002
|
+
// Define routes on the router
|
|
1003
|
+
usersRouter.get('/', (req, res) => {
|
|
1004
|
+
res.json({ users: [] });
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
usersRouter.get('/:id', (req, res) => {
|
|
1008
|
+
res.json({ user: { id: req.params.id } });
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
usersRouter.post('/', (req, res) => {
|
|
1012
|
+
res.status(201).json({ created: true });
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// Mount the router
|
|
1016
|
+
const app = Lieko();
|
|
1017
|
+
app.use('/api/users', usersRouter);
|
|
1018
|
+
|
|
1019
|
+
// Now accessible at:
|
|
1020
|
+
// GET /api/users
|
|
1021
|
+
// GET /api/users/:id
|
|
1022
|
+
// POST /api/users
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### Nested Routers
|
|
1026
|
+
|
|
1027
|
+
```javascript
|
|
1028
|
+
const apiRouter = Lieko.Router();
|
|
1029
|
+
const usersRouter = Lieko.Router();
|
|
1030
|
+
const postsRouter = Lieko.Router();
|
|
1031
|
+
|
|
1032
|
+
// Users routes
|
|
1033
|
+
usersRouter.get('/', (req, res) => res.json({ users: [] }));
|
|
1034
|
+
usersRouter.get('/:id', (req, res) => res.json({ user: {} }));
|
|
1035
|
+
|
|
1036
|
+
// Posts routes
|
|
1037
|
+
postsRouter.get('/', (req, res) => res.json({ posts: [] }));
|
|
1038
|
+
postsRouter.get('/:id', (req, res) => res.json({ post: {} }));
|
|
1039
|
+
|
|
1040
|
+
// Mount sub-routers on API router
|
|
1041
|
+
apiRouter.use('/users', usersRouter);
|
|
1042
|
+
apiRouter.use('/posts', postsRouter);
|
|
1043
|
+
|
|
1044
|
+
// Mount API router on app
|
|
1045
|
+
app.use('/api', apiRouter);
|
|
1046
|
+
|
|
1047
|
+
// Available routes:
|
|
1048
|
+
// /api/users
|
|
1049
|
+
// /api/users/:id
|
|
1050
|
+
// /api/posts
|
|
1051
|
+
// /api/posts/:id
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Router with Middlewares
|
|
1055
|
+
|
|
1056
|
+
```javascript
|
|
1057
|
+
const adminRouter = Lieko.Router();
|
|
1058
|
+
|
|
1059
|
+
// Middleware for all admin routes
|
|
1060
|
+
const adminAuth = (req, res, next) => {
|
|
1061
|
+
if (req.user && req.user.role === 'admin') {
|
|
1062
|
+
next();
|
|
1063
|
+
} else {
|
|
1064
|
+
res.error({ code: 'FORBIDDEN', message: 'Admin access required' });
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
adminRouter.get('/dashboard', (req, res) => {
|
|
1069
|
+
res.json({ dashboard: 'data' });
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
adminRouter.post('/settings', (req, res) => {
|
|
1073
|
+
res.json({ updated: true });
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Mount with middleware
|
|
1077
|
+
app.use('/admin', authMiddleware, adminAuth, adminRouter);
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Modular Application Structure
|
|
1081
|
+
|
|
1082
|
+
```javascript
|
|
1083
|
+
// routes/users.js
|
|
1084
|
+
const Lieko = require('lieko-express');
|
|
1085
|
+
const router = Lieko.Router();
|
|
1086
|
+
|
|
1087
|
+
router.get('/', (req, res) => { /* ... */ });
|
|
1088
|
+
router.post('/', (req, res) => { /* ... */ });
|
|
1089
|
+
|
|
1090
|
+
module.exports = router;
|
|
1091
|
+
|
|
1092
|
+
// routes/posts.js
|
|
1093
|
+
const Lieko = require('lieko-express');
|
|
1094
|
+
const router = Lieko.Router();
|
|
1095
|
+
|
|
1096
|
+
router.get('/', (req, res) => { /* ... */ });
|
|
1097
|
+
router.post('/', (req, res) => { /* ... */ });
|
|
1098
|
+
|
|
1099
|
+
module.exports = router;
|
|
1100
|
+
|
|
1101
|
+
// app.js
|
|
1102
|
+
const Lieko = require('lieko-express');
|
|
1103
|
+
const usersRouter = require('./routes/users');
|
|
1104
|
+
const postsRouter = require('./routes/posts');
|
|
1105
|
+
|
|
1106
|
+
const app = Lieko();
|
|
1107
|
+
|
|
1108
|
+
app.use('/api/users', usersRouter);
|
|
1109
|
+
app.use('/api/posts', postsRouter);
|
|
1110
|
+
|
|
1111
|
+
app.listen(3000);
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
## ⚠️ Error Handling
|
|
1115
|
+
|
|
1116
|
+
### Custom 404 Handler
|
|
1117
|
+
|
|
1118
|
+
```javascript
|
|
1119
|
+
app.notFound((req, res) => {
|
|
1120
|
+
res.status(404).json({
|
|
1121
|
+
error: 'Not Found',
|
|
1122
|
+
message: `Route ${req.method} ${req.url} does not exist`,
|
|
1123
|
+
timestamp: new Date().toISOString()
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
### Error Response Helper
|
|
1129
|
+
|
|
1130
|
+
```javascript
|
|
1131
|
+
app.get('/users/:id', (req, res) => {
|
|
1132
|
+
const user = findUser(req.params.id);
|
|
1133
|
+
|
|
1134
|
+
if (!user) {
|
|
1135
|
+
return res.error({
|
|
1136
|
+
code: 'NOT_FOUND',
|
|
1137
|
+
message: 'User not found'
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
res.json({ user });
|
|
1142
|
+
});
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
### Standard Error Codes
|
|
1146
|
+
|
|
1147
|
+
The `res.error()` helper automatically maps error codes to HTTP status codes:
|
|
1148
|
+
|
|
1149
|
+
```javascript
|
|
1150
|
+
// 4xx - Client Errors
|
|
1151
|
+
res.error({ code: 'INVALID_REQUEST' }); // 400
|
|
1152
|
+
res.error({ code: 'VALIDATION_FAILED' }); // 400
|
|
1153
|
+
res.error({ code: 'NO_TOKEN_PROVIDED' }); // 401
|
|
1154
|
+
res.error({ code: 'INVALID_TOKEN' }); // 401
|
|
1155
|
+
res.error({ code: 'FORBIDDEN' }); // 403
|
|
1156
|
+
res.error({ code: 'NOT_FOUND' }); // 404
|
|
1157
|
+
res.error({ code: 'METHOD_NOT_ALLOWED' }); // 405
|
|
1158
|
+
res.error({ code: 'CONFLICT' }); // 409
|
|
1159
|
+
res.error({ code: 'RECORD_EXISTS' }); // 409
|
|
1160
|
+
res.error({ code: 'TOO_MANY_REQUESTS' }); // 429
|
|
1161
|
+
|
|
1162
|
+
// 5xx - Server Errors
|
|
1163
|
+
res.error({ code: 'SERVER_ERROR' }); // 500
|
|
1164
|
+
res.error({ code: 'SERVICE_UNAVAILABLE' }); // 503
|
|
1165
|
+
|
|
1166
|
+
// Custom status
|
|
1167
|
+
res.error({ code: 'CUSTOM_ERROR', status: 418 });
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
### Error Response Format
|
|
1171
|
+
|
|
1172
|
+
```json
|
|
1173
|
+
{
|
|
1174
|
+
"success": false,
|
|
1175
|
+
"error": {
|
|
1176
|
+
"code": "NOT_FOUND",
|
|
1177
|
+
"message": "User not found"
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
## 🔥 Advanced Examples
|
|
1184
|
+
|
|
1185
|
+
### Complete REST API
|
|
1186
|
+
|
|
1187
|
+
```javascript
|
|
1188
|
+
const Lieko = require('lieko-express');
|
|
1189
|
+
const { Schema, validators, validate } = require('lieko-express');
|
|
1190
|
+
|
|
1191
|
+
const app = Lieko();
|
|
1192
|
+
|
|
1193
|
+
// Database (in-memory)
|
|
1194
|
+
const db = {
|
|
1195
|
+
users: [
|
|
1196
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
1197
|
+
]
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
let nextId = 2;
|
|
1201
|
+
|
|
1202
|
+
// Validation schema
|
|
1203
|
+
const userSchema = schema({
|
|
1204
|
+
name: [
|
|
1205
|
+
validators.required('Name is required'),
|
|
1206
|
+
validators.minLength(2, 'Name must be at least 2 characters')
|
|
1207
|
+
],
|
|
1208
|
+
email: [
|
|
1209
|
+
validators.required('Email is required'),
|
|
1210
|
+
validators.email('Invalid email format')
|
|
1211
|
+
]
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
// Middlewares
|
|
1215
|
+
app.use((req, res, next) => {
|
|
1216
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
|
1217
|
+
next();
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// Routes
|
|
1221
|
+
app.get('/api/users', (req, res) => {
|
|
1222
|
+
res.ok(db.users, 'Users retrieved successfully');
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
app.get('/api/users/:id', (req, res) => {
|
|
1226
|
+
const user = db.users.find(u => u.id === parseInt(req.params.id));
|
|
1227
|
+
|
|
1228
|
+
if (!user) {
|
|
1229
|
+
return res.error({ code: 'NOT_FOUND', message: 'User not found' });
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
res.ok(user);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
app.post('/api/users', validate(userSchema), (req, res) => {
|
|
1236
|
+
const { name, email } = req.body;
|
|
1237
|
+
|
|
1238
|
+
// Check if email exists
|
|
1239
|
+
const exists = db.users.some(u => u.email === email);
|
|
1240
|
+
if (exists) {
|
|
1241
|
+
return res.error({
|
|
1242
|
+
code: 'RECORD_EXISTS',
|
|
1243
|
+
message: 'Email already registered'
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const newUser = {
|
|
1248
|
+
id: nextId++,
|
|
1249
|
+
name,
|
|
1250
|
+
email
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
db.users.push(newUser);
|
|
1254
|
+
res.status(201).ok(newUser, 'User created successfully');
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
app.put('/api/users/:id', validate(userSchema), (req, res) => {
|
|
1258
|
+
const id = parseInt(req.params.id);
|
|
1259
|
+
const index = db.users.findIndex(u => u.id === id);
|
|
1260
|
+
|
|
1261
|
+
if (index === -1) {
|
|
1262
|
+
return res.error({ code: 'NOT_FOUND', message: 'User not found' });
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
db.users[index] = { ...db.users[index], ...req.body, id };
|
|
1266
|
+
res.ok(db.users[index], 'User updated successfully');
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
app.delete('/api/users/:id', (req, res) => {
|
|
1270
|
+
const id = parseInt(req.params.id);
|
|
1271
|
+
const index = db.users.findIndex(u => u.id === id);
|
|
1272
|
+
|
|
1273
|
+
if (index === -1) {
|
|
1274
|
+
return res.error({ code: 'NOT_FOUND', message: 'User not found' });
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
db.users.splice(index, 1);
|
|
1278
|
+
res.ok({ deleted: true }, 'User deleted successfully');
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// 404 handler
|
|
1282
|
+
app.notFound((req, res) => {
|
|
1283
|
+
res.status(404).json({
|
|
1284
|
+
success: false,
|
|
1285
|
+
error: {
|
|
1286
|
+
code: 'NOT_FOUND',
|
|
1287
|
+
message: `Route ${req.method} ${req.url} not found`
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
app.listen(3000, () => {
|
|
1293
|
+
console.log('🚀 Server running on http://localhost:3000');
|
|
1294
|
+
});
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
### Authentication & Authorization
|
|
1298
|
+
|
|
1299
|
+
```javascript
|
|
1300
|
+
const Lieko = require('lieko-express');
|
|
1301
|
+
const app = Lieko();
|
|
1302
|
+
|
|
1303
|
+
// Mock user database
|
|
1304
|
+
const users = [
|
|
1305
|
+
{ id: 1, username: 'admin', password: 'admin123', role: 'admin' },
|
|
1306
|
+
{ id: 2, username: 'user', password: 'user123', role: 'user' }
|
|
1307
|
+
];
|
|
1308
|
+
|
|
1309
|
+
const sessions = {}; // token -> user
|
|
1310
|
+
|
|
1311
|
+
// Auth middleware
|
|
1312
|
+
const authMiddleware = (req, res, next) => {
|
|
1313
|
+
const token = req.headers['authorization']?.replace('Bearer ', '');
|
|
1314
|
+
|
|
1315
|
+
if (!token) {
|
|
1316
|
+
return res.error({
|
|
1317
|
+
code: 'NO_TOKEN_PROVIDED',
|
|
1318
|
+
message: 'Authorization token required'
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const user = sessions[token];
|
|
1323
|
+
|
|
1324
|
+
if (!user) {
|
|
1325
|
+
return res.error({
|
|
1326
|
+
code: 'INVALID_TOKEN',
|
|
1327
|
+
message: 'Invalid or expired token'
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
req.user = user;
|
|
1332
|
+
next();
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
// Role check middleware
|
|
1336
|
+
const requireRole = (...roles) => {
|
|
1337
|
+
return (req, res, next) => {
|
|
1338
|
+
if (!roles.includes(req.user.role)) {
|
|
1339
|
+
return res.error({
|
|
1340
|
+
code: 'FORBIDDEN',
|
|
1341
|
+
message: 'Insufficient permissions'
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
next();
|
|
1345
|
+
};
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
// Login route
|
|
1349
|
+
app.post('/auth/login', (req, res) => {
|
|
1350
|
+
const { username, password } = req.body;
|
|
1351
|
+
|
|
1352
|
+
const user = users.find(u =>
|
|
1353
|
+
u.username === username && u.password === password
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
if (!user) {
|
|
1357
|
+
return res.error({
|
|
1358
|
+
code: 'INVALID_CREDENTIALS',
|
|
1359
|
+
status: 401,
|
|
1360
|
+
message: 'Invalid username or password'
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Generate token (in production, use JWT)
|
|
1365
|
+
const token = Math.random().toString(36).substring(7);
|
|
1366
|
+
sessions[token] = { id: user.id, username: user.username, role: user.role };
|
|
1367
|
+
|
|
1368
|
+
res.ok({
|
|
1369
|
+
token,
|
|
1370
|
+
user: { id: user.id, username: user.username, role: user.role }
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
// Protected route
|
|
1375
|
+
app.get('/api/profile', authMiddleware, (req, res) => {
|
|
1376
|
+
res.ok({ user: req.user });
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// Admin-only route
|
|
1380
|
+
app.get('/api/admin/stats', authMiddleware, requireRole('admin'), (req, res) => {
|
|
1381
|
+
res.ok({ totalUsers: users.length, sessions: Object.keys(sessions).length });
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Logout
|
|
1385
|
+
app.post('/auth/logout', authMiddleware, (req, res) => {
|
|
1386
|
+
const token = req.headers['authorization']?.replace('Bearer ', '');
|
|
1387
|
+
delete sessions[token];
|
|
1388
|
+
res.ok({ message: 'Logged out successfully' });
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
app.listen(3000);
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
### File Upload Handler
|
|
1395
|
+
|
|
1396
|
+
```javascript
|
|
1397
|
+
const Lieko = require('lieko-express');
|
|
1398
|
+
const { writeFileSync } = require('fs');
|
|
1399
|
+
const { join } = require('path');
|
|
1400
|
+
|
|
1401
|
+
const app = Lieko();
|
|
1402
|
+
|
|
1403
|
+
app.post('/upload', (req, res) => {
|
|
1404
|
+
if (!req.files || !req.files.file) {
|
|
1405
|
+
return res.error({
|
|
1406
|
+
code: 'INVALID_REQUEST',
|
|
1407
|
+
message: 'No file uploaded'
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const file = req.files.file;
|
|
1412
|
+
|
|
1413
|
+
// Validate file type
|
|
1414
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
|
1415
|
+
if (!allowedTypes.includes(file.contentType)) {
|
|
1416
|
+
return res.error({
|
|
1417
|
+
code: 'INVALID_REQUEST',
|
|
1418
|
+
message: 'Only JPEG, PNG and GIF files are allowed'
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Validate file size (5MB max)
|
|
1423
|
+
const maxSize = 5 * 1024 * 1024;
|
|
1424
|
+
if (file.data.length > maxSize) {
|
|
1425
|
+
return res.error({
|
|
1426
|
+
code: 'INVALID_REQUEST',
|
|
1427
|
+
message: 'File size must not exceed 5MB'
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Save file
|
|
1432
|
+
const filename = `${Date.now()}-${file.filename}`;
|
|
1433
|
+
const filepath = join(__dirname, 'uploads', filename);
|
|
1434
|
+
|
|
1435
|
+
writeFileSync(filepath, file.data);
|
|
1436
|
+
|
|
1437
|
+
res.ok({
|
|
1438
|
+
filename,
|
|
1439
|
+
originalName: file.filename,
|
|
1440
|
+
size: file.data.length,
|
|
1441
|
+
contentType: file.contentType,
|
|
1442
|
+
url: `/uploads/${filename}`
|
|
1443
|
+
}, 'File uploaded successfully');
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
app.listen(3000);
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
### Rate Limiting Middleware
|
|
1450
|
+
|
|
1451
|
+
```javascript
|
|
1452
|
+
const rateLimits = new Map();
|
|
1453
|
+
|
|
1454
|
+
const rateLimit = (options = {}) => {
|
|
1455
|
+
const {
|
|
1456
|
+
windowMs = 60000, // 1 minute
|
|
1457
|
+
maxRequests = 100
|
|
1458
|
+
} = options;
|
|
1459
|
+
|
|
1460
|
+
return (req, res, next) => {
|
|
1461
|
+
const key = req.ip;
|
|
1462
|
+
const now = Date.now();
|
|
1463
|
+
|
|
1464
|
+
if (!rateLimits.has(key)) {
|
|
1465
|
+
rateLimits.set(key, { count: 1, resetTime: now + windowMs });
|
|
1466
|
+
return next();
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const limit = rateLimits.get(key);
|
|
1470
|
+
|
|
1471
|
+
if (now > limit.resetTime) {
|
|
1472
|
+
limit.count = 1;
|
|
1473
|
+
limit.resetTime = now + windowMs;
|
|
1474
|
+
return next();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (limit.count >= maxRequests) {
|
|
1478
|
+
return res.error({
|
|
1479
|
+
code: 'TOO_MANY_REQUESTS',
|
|
1480
|
+
message: 'Rate limit exceeded. Please try again later.'
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
limit.count++;
|
|
1485
|
+
next();
|
|
1486
|
+
};
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// Usage
|
|
1490
|
+
app.use('/api', rateLimit({ windowMs: 60000, maxRequests: 100 }));
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
### Request Logging Middleware
|
|
1494
|
+
|
|
1495
|
+
```javascript
|
|
1496
|
+
const requestLogger = (req, res, next) => {
|
|
1497
|
+
const start = Date.now();
|
|
1498
|
+
|
|
1499
|
+
// Capture original end method
|
|
1500
|
+
const originalEnd = res.end;
|
|
1501
|
+
|
|
1502
|
+
res.end = function(...args) {
|
|
1503
|
+
const duration = Date.now() - start;
|
|
1504
|
+
|
|
1505
|
+
console.log({
|
|
1506
|
+
timestamp: new Date().toISOString(),
|
|
1507
|
+
method: req.method,
|
|
1508
|
+
url: req.url,
|
|
1509
|
+
status: res.statusCode,
|
|
1510
|
+
duration: `${duration}ms`,
|
|
1511
|
+
ip: req.ip,
|
|
1512
|
+
userAgent: req.headers['user-agent']
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
originalEnd.apply(res, args);
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
next();
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
app.use(requestLogger);
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
## 🎯 Complete Application Example
|
|
1526
|
+
|
|
1527
|
+
See the [examples](./examples) directory for a full-featured application with:
|
|
1528
|
+
- User authentication
|
|
1529
|
+
- CRUD operations
|
|
1530
|
+
- Validation
|
|
1531
|
+
- File uploads
|
|
1532
|
+
- Nested routers
|
|
1533
|
+
- Middleware examples
|
|
1534
|
+
- Comprehensive test suite
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
# 📊 Performance Tips (Suite)
|
|
1538
|
+
|
|
1539
|
+
1. **Use async middlewares** for I/O operations
|
|
1540
|
+
2. **Avoid heavy synchronous operations** inside request handlers
|
|
1541
|
+
3. **Minimize deep-nested routers** unless needed
|
|
1542
|
+
4. **Reuse validation schemas** instead of re-creating them for each request
|
|
1543
|
+
5. **Use reverse proxy headers correctly** (`trust proxy`) when hosting behind Nginx
|
|
1544
|
+
6. **Disable console logs in production** or use a real logger with adjustable log levels
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
## Debug & Introspection Tools
|
|
1548
|
+
|
|
1549
|
+
Lieko Express comes with powerful built-in development and debugging utilities.
|
|
1550
|
+
|
|
1551
|
+
### 🐞 Enable Debug Mode
|
|
1552
|
+
|
|
1553
|
+
```js
|
|
1554
|
+
const app = Lieko();
|
|
1555
|
+
|
|
1556
|
+
// Enable debug mode (never use in production!)
|
|
1557
|
+
app.settings.debug = true;
|
|
1558
|
+
// or simply
|
|
1559
|
+
app.enable('debug')
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
When `debug` is enabled, Lieko automatically logs every request with a beautiful, color-coded output containing:
|
|
1563
|
+
|
|
1564
|
+
- Method + full URL
|
|
1565
|
+
- Real client IP (IPv4/IPv6)
|
|
1566
|
+
- Status code (color-coded)
|
|
1567
|
+
- Ultra-precise response time (µs / ms / s)
|
|
1568
|
+
- Route parameters, query string, body, uploaded files
|
|
1569
|
+
|
|
1570
|
+
**Example output:**
|
|
1571
|
+
```
|
|
1572
|
+
DEBUG REQUEST
|
|
1573
|
+
→ GET /api/users/42?page=2&active=true
|
|
1574
|
+
→ IP: 127.0.0.1
|
|
1575
|
+
→ Status: 200
|
|
1576
|
+
→ Duration: 1.847ms
|
|
1577
|
+
→ Params: {"id":"42"}
|
|
1578
|
+
→ Query: {"page":2,"active":true}
|
|
1579
|
+
→ Body: {}
|
|
1580
|
+
→ Files:
|
|
1581
|
+
---------------------------------------------
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
**Warning:** Never enable `app.enable('debug')` in production (performance impact + potential sensitive data leak).
|
|
1585
|
+
|
|
1586
|
+
### List All Registered Routes
|
|
1587
|
+
|
|
1588
|
+
```js
|
|
1589
|
+
// Returns an array of route objects — perfect for tests or auto-docs
|
|
1590
|
+
const routes = app.listRoutes();
|
|
1591
|
+
console.log(routes);
|
|
1592
|
+
/*
|
|
1593
|
+
[
|
|
1594
|
+
{ method: 'GET', path: '/api/users', middlewares: 1 },
|
|
1595
|
+
{ method: 'POST', path: '/api/users', middlewares: 2 },
|
|
1596
|
+
{ method: 'GET', path: '/api/users/:id', middlewares: 0 },
|
|
1597
|
+
{ method: 'DELETE', path: '/api/users/:id', middlewares: 1 },
|
|
1598
|
+
...
|
|
1599
|
+
]
|
|
1600
|
+
*/
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1603
|
+
Ideal for generating OpenAPI specs, runtime validation, or integration tests.
|
|
1604
|
+
|
|
1605
|
+
### Pretty-Print All Routes in Console
|
|
1606
|
+
|
|
1607
|
+
```js
|
|
1608
|
+
// Display a clean table
|
|
1609
|
+
app.printRoutes();
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
**Sample output:**
|
|
1613
|
+
```
|
|
1614
|
+
Registered Routes:
|
|
1615
|
+
|
|
1616
|
+
GET /api/users (middlewares: 1)
|
|
1617
|
+
POST /api/users (middlewares: 2)
|
|
1618
|
+
GET /api/users/:id (middlewares: 0)
|
|
1619
|
+
DELETE /api/users/:id (middlewares: 1)
|
|
1620
|
+
PATCH /api/users/:id (middlewares: 0)
|
|
1621
|
+
ALL /webhook/* (middlewares: 0)
|
|
1622
|
+
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
### Nested (group-based):
|
|
1626
|
+
|
|
1627
|
+
```js
|
|
1628
|
+
app.printRoutesNested();
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
Example:
|
|
1632
|
+
|
|
1633
|
+
```
|
|
1634
|
+
/api [auth]
|
|
1635
|
+
/api/admin [auth, requireAdmin]
|
|
1636
|
+
GET /users
|
|
1637
|
+
POST /users
|
|
1638
|
+
GET /profile
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
Perfect to quickly verify that all your routes and middlewares are correctly registered.
|
|
1642
|
+
|
|
1643
|
+
## ⚙️ Application Settings
|
|
1644
|
+
|
|
1645
|
+
Lieko Express provides a small but effective settings mechanism.
|
|
1646
|
+
|
|
1647
|
+
You can configure:
|
|
1648
|
+
|
|
1649
|
+
```js
|
|
1650
|
+
app.set('trust proxy', value);
|
|
1651
|
+
app.set('debug', boolean);
|
|
1652
|
+
app.set('x-powered-by', false);
|
|
1653
|
+
app.set('strictTrailingSlash', false);
|
|
1654
|
+
app.set('allowTrailingSlash', true);
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
# 🌐 Trust Proxy & IP Parsing
|
|
1658
|
+
|
|
1659
|
+
Lieko improves on Express with a powerful trust proxy system.
|
|
1660
|
+
|
|
1661
|
+
### Configure:
|
|
1662
|
+
|
|
1663
|
+
```js
|
|
1664
|
+
app.set('trust proxy', true);
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
### Supported values:
|
|
1668
|
+
|
|
1669
|
+
* `true` — trust all
|
|
1670
|
+
* `"loopback"`
|
|
1671
|
+
* `"127.0.0.1"`
|
|
1672
|
+
* `["10.0.0.1", "10.0.0.2"]`
|
|
1673
|
+
* custom function `(ip) => boolean`
|
|
1674
|
+
|
|
1675
|
+
### Provided fields:
|
|
1676
|
+
|
|
1677
|
+
```js
|
|
1678
|
+
req.ip.raw
|
|
1679
|
+
req.ip.ipv4
|
|
1680
|
+
req.ip.ipv6
|
|
1681
|
+
req.ips // full proxy chain
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
Lieko correctly handles:
|
|
1685
|
+
|
|
1686
|
+
* IPv6
|
|
1687
|
+
* IPv4-mapped IPv6 (`::ffff:127.0.0.1`)
|
|
1688
|
+
* Multi-proxy headers
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
## 🧩 Internals & Architecture
|
|
1692
|
+
|
|
1693
|
+
This section describes how Lieko Express works under the hood.
|
|
1694
|
+
|
|
1695
|
+
### Request Lifecycle
|
|
1696
|
+
|
|
1697
|
+
1. Enhance request object (IP parsing, protocol, host…)
|
|
1698
|
+
2. Parse query string
|
|
1699
|
+
3. Parse body (`json`, `urlencoded`, `multipart/form-data`, or text)
|
|
1700
|
+
4. Apply **global middlewares**
|
|
1701
|
+
5. Match route using regex-based router
|
|
1702
|
+
6. Apply **route middlewares**
|
|
1703
|
+
7. Execute route handler
|
|
1704
|
+
8. Apply 404 handler if no route matched
|
|
1705
|
+
9. Send error responses using the built-in error system
|
|
1706
|
+
|
|
1707
|
+
### Router Internals
|
|
1708
|
+
|
|
1709
|
+
Routes are converted to regular expressions:
|
|
1710
|
+
|
|
1711
|
+
```txt
|
|
1712
|
+
/users/:id → ^/users/(?<id>[^/]+)$
|
|
1713
|
+
/files/* → ^/files/.*$
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
This allows:
|
|
1717
|
+
|
|
1718
|
+
* Named parameters
|
|
1719
|
+
* Wildcards
|
|
1720
|
+
* Fast matching
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
## 🧱 Extending Lieko Express
|
|
1724
|
+
|
|
1725
|
+
Because the framework is intentionally small, you can easily extend it.
|
|
1726
|
+
|
|
1727
|
+
### Custom Response Helpers
|
|
1728
|
+
|
|
1729
|
+
```js
|
|
1730
|
+
app.use((req, res, next) => {
|
|
1731
|
+
res.created = (data) => {
|
|
1732
|
+
res.status(201).json({ success: true, data });
|
|
1733
|
+
};
|
|
1734
|
+
next();
|
|
1735
|
+
});
|
|
1736
|
+
```
|
|
1737
|
+
|
|
1738
|
+
### Custom Middlewares
|
|
1739
|
+
|
|
1740
|
+
```js
|
|
1741
|
+
const timing = (req, res, next) => {
|
|
1742
|
+
const start = Date.now();
|
|
1743
|
+
res.end = ((original) => (...args) => {
|
|
1744
|
+
console.log(`⏱️ ${req.method} ${req.url} took ${Date.now() - start}ms`);
|
|
1745
|
+
original(...args);
|
|
1746
|
+
})(res.end);
|
|
1747
|
+
next();
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
app.use(timing);
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
# 🔌 Plugins
|
|
1754
|
+
|
|
1755
|
+
A plugin is simply a function receiving `app`:
|
|
1756
|
+
|
|
1757
|
+
```js
|
|
1758
|
+
function myPlugin(app) {
|
|
1759
|
+
app.get('/plugin-test', (req, res) => res.ok("OK"));
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
myPlugin(app);
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
## 🚀 Deploying Lieko Express
|
|
1766
|
+
|
|
1767
|
+
### Node.js (PM2)
|
|
1768
|
+
|
|
1769
|
+
```bash
|
|
1770
|
+
pm2 start app.js
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
## 📦 API Reference
|
|
1774
|
+
|
|
1775
|
+
### `Lieko()`
|
|
1776
|
+
|
|
1777
|
+
Creates a new application instance.
|
|
1778
|
+
|
|
1779
|
+
### `Lieko.Router()`
|
|
1780
|
+
|
|
1781
|
+
Creates a modular router (internally: a Lieko app).
|
|
1782
|
+
|
|
1783
|
+
### `app.use(...)`
|
|
1784
|
+
|
|
1785
|
+
Supported patterns:
|
|
1786
|
+
|
|
1787
|
+
| Signature | Description |
|
|
1788
|
+
| --------------------------------- | ------------------------ |
|
|
1789
|
+
| `app.use(fn)` | Global middleware |
|
|
1790
|
+
| `app.use(path, fn)` | Path-filtered middleware |
|
|
1791
|
+
| `app.use(path, router)` | Mount router |
|
|
1792
|
+
| `app.use(path, mw1, mw2, router)` | Mix middlewares + router |
|
|
1793
|
+
|
|
1794
|
+
### `app.get/post/put/delete/patch/all(path, ...handlers)`
|
|
1795
|
+
|
|
1796
|
+
Registers routes with optional middlewares.
|
|
1797
|
+
|
|
1798
|
+
### `app.notFound(handler)`
|
|
1799
|
+
|
|
1800
|
+
Custom 404 callback.
|
|
1801
|
+
|
|
1802
|
+
```js
|
|
1803
|
+
app.notFound((req, res) => {
|
|
1804
|
+
res.error({
|
|
1805
|
+
code: "NOT_FOUND",
|
|
1806
|
+
message: `Route "${req.method} ${req.originalUrl}" does not exist`
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// With logging
|
|
1811
|
+
app.notFound((req, res) => {
|
|
1812
|
+
console.warn(`404: ${req.method} ${req.originalUrl}`);
|
|
1813
|
+
res.error("ROUTE_NOT_FOUND");
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
// With HTML Response
|
|
1817
|
+
app.notFound((req, res) => {
|
|
1818
|
+
res.status(404).send("<h1>Page Not Found</h1>");
|
|
1819
|
+
});
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
### `app.errorHandler(handler)`
|
|
1823
|
+
|
|
1824
|
+
Register a custom 500 handler.
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
## 🔍 Known Limitations
|
|
1829
|
+
|
|
1830
|
+
Because Lieko Express is minimalistic:
|
|
1831
|
+
|
|
1832
|
+
* No template engine
|
|
1833
|
+
* No streaming uploads for multipart/form-data (parsed in memory)
|
|
1834
|
+
* No built-in cookies/sessions
|
|
1835
|
+
* No WebSocket support yet
|
|
1836
|
+
* Routers cannot have their own `notFound` handler (inherited from parent)
|
|
1837
|
+
|
|
1838
|
+
Future versions may address some of these.
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
## 🤝 Contributing
|
|
1842
|
+
|
|
1843
|
+
Contributions are welcome!
|
|
1844
|
+
|
|
1845
|
+
1. Fork the repository
|
|
1846
|
+
2. Create a feature branch
|
|
1847
|
+
3. Commit your changes
|
|
1848
|
+
4. Open a pull request
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
## 📄 License
|
|
1853
|
+
|
|
1854
|
+
MIT License — free to use in personal and commercial projects.
|