json-rest-api 1.0.0 → 1.0.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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
package/README.md CHANGED
@@ -1,427 +1,773 @@
1
- # JSON REST API
1
+ # JSON REST API Tutorial
2
2
 
3
- A modern, plugin-based JSON REST API library with JSON:API compliance, schema validation, and extensible storage backends.
3
+ This tutorial will guide you through building a REST API using the `jsonrestapi` package and `hooked-api`. We'll start with the basics and gradually add more features.
4
4
 
5
- ## Features
5
+ ## Table of Contents
6
6
 
7
- - **Plugin Architecture**: Extensible through plugins using `.use()` pattern
8
- - **JSON:API Compliant**: Follows JSON:API specification for RESTful APIs
9
- - **Schema Validation**: Built-in schema validation with extensible types and parameters
10
- - **Multiple Storage Backends**: Memory and MySQL support out of the box
11
- - **Automatic API Versioning**: Built-in version management with automatic routing
12
- - **Positioning**: Manage record ordering with drag-and-drop support
13
- - **Resource Versioning**: Track changes with version history
14
- - **TypeScript-like Schema**: Define schemas with types, validation rules, and defaults
15
- - **Hooks System**: Extensible through before/after hooks
16
- - **Express Integration**: Easy integration with Express.js
17
- - **Security**: Built-in security plugin with rate limiting, CORS, and authentication
18
- - **Logging**: Structured logging with sensitive data redaction
19
- - **OpenAPI**: Auto-generated API documentation
7
+ 1. [Getting Started](#getting-started)
8
+ 2. [Basic REST API Plugin](#basic-rest-api-plugin)
9
+ 3. [Connector Plugins](#connector-plugins)
10
+ - [Express Plugin](#express-plugin)
11
+ - [HTTP Plugin](#http-plugin)
12
+ 4. [File Storage](#file-storage)
13
+ - [File Uploads with Express](#file-uploads-with-express)
14
+ - [Storage Adapters](#storage-adapters)
20
15
 
21
- ## Installation
16
+ ## Getting Started
17
+
18
+ First, let's install the required packages:
22
19
 
23
20
  ```bash
24
- npm install jsonrestapi
21
+ npm install hooked-api jsonrestapi
25
22
  ```
26
23
 
27
- ## Quick Start
24
+ Create a new file called `api.js`:
28
25
 
29
26
  ```javascript
30
- import { createApi, Schema } from 'jsonrestapi';
31
- import express from 'express';
27
+ import { Api } from 'hooked-api';
32
28
 
33
- // Create an API with automatic versioning
34
- const api = createApi({
35
- name: 'myapp',
36
- version: '1.0.0',
37
- storage: 'memory',
38
- http: { basePath: '/api' }
29
+ // Create a new API instance
30
+ const api = new Api({
31
+ name: 'my-library-api',
32
+ version: '1.0.0'
39
33
  });
34
+ ```
40
35
 
41
- // Define a schema
42
- const userSchema = new Schema({
43
- id: { type: 'id' },
44
- name: { type: 'string', required: true, min: 2, max: 100 },
45
- email: { type: 'string', required: true, lowercase: true },
46
- active: { type: 'boolean', default: true }
47
- });
36
+ ## Basic REST API Plugin
48
37
 
49
- // Register the schema
50
- api.addResource('users', userSchema);
38
+ The `RestApiPlugin` is the foundation of jsonrestapi. It adds REST methods (query, get, post, put, patch, delete) to your API scopes.
51
39
 
52
- // Create Express app and mount API
53
- const app = express();
54
- api.mount(app);
40
+ ### Adding the REST API Plugin
55
41
 
56
- app.listen(3000);
57
- ```
42
+ ```javascript
43
+ import { Api } from 'hooked-api';
44
+ import { RestApiPlugin } from 'json-rest-api';
58
45
 
59
- ## Core Concepts
46
+ const api = new Api({
47
+ name: 'my-library-api',
48
+ version: '1.0.0'
49
+ });
60
50
 
61
- ### Schema Definition
51
+ // Use the REST API plugin
52
+ api.use(RestApiPlugin);
53
+ ```
54
+
55
+ ### Creating a Books Resource
62
56
 
63
- Schemas define the structure and validation rules for your data:
57
+ Now let's create a simple "books" resource with fake data:
64
58
 
65
59
  ```javascript
66
- const schema = new Schema({
67
- // Numeric types
68
- id: { type: 'id' },
69
- age: { type: 'number', min: 0, max: 150 },
70
- price: { type: 'number', currency: true },
71
-
72
- // String types
73
- name: { type: 'string', required: true, min: 2, max: 50 },
74
- email: { type: 'string', lowercase: true },
75
- description: { type: 'string', max: 1000 },
76
-
77
- // Boolean type
78
- active: { type: 'boolean', default: true },
60
+ // Define a books resource
61
+ api.addResource('books', {
62
+ schema: {
63
+ title: { type: 'string', required: true },
64
+ author: { type: 'string', required: true },
65
+ year: { type: 'number' },
66
+ isbn: { type: 'string' }
67
+ }
68
+ });
69
+
70
+ // Add simple data helpers (in real apps, these would connect to a database)
71
+ api.vars.helpers.dataQuery = async ({ scopeName, queryParams }) => {
72
+ // For now, just return some fake books
73
+ const fakeBooks = [
74
+ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
75
+ { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
76
+ { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
77
+ ];
79
78
 
80
- // Date types
81
- createdAt: { type: 'timestamp', default: () => Date.now() },
82
- birthDate: { type: 'date' },
83
- lastLogin: { type: 'dateTime' },
79
+ return {
80
+ data: fakeBooks.map(book => ({
81
+ type: 'books',
82
+ id: book.id,
83
+ attributes: {
84
+ title: book.title,
85
+ author: book.author,
86
+ year: book.year
87
+ }
88
+ }))
89
+ };
90
+ };
91
+
92
+ api.vars.helpers.dataGet = async ({ scopeName, id }) => {
93
+ // Return a single fake book
94
+ const book = { id, title: 'Example Book', author: 'Example Author', year: 2024 };
84
95
 
85
- // Complex types
86
- tags: { type: 'array', default: [] },
87
- metadata: { type: 'object' },
96
+ return {
97
+ data: {
98
+ type: 'books',
99
+ id: book.id,
100
+ attributes: {
101
+ title: book.title,
102
+ author: book.author,
103
+ year: book.year
104
+ }
105
+ }
106
+ };
107
+ };
108
+
109
+ api.vars.helpers.dataPost = async ({ scopeName, inputRecord }) => {
110
+ // Pretend to create a book and return it
111
+ const newBook = {
112
+ id: String(Date.now()), // Simple ID generation
113
+ ...inputRecord.data.attributes
114
+ };
88
115
 
89
- // Special types
90
- data: { type: 'serialize' }, // Circular JSON support
91
- file: { type: 'blob' }
92
- });
93
- ```
116
+ return {
117
+ data: {
118
+ type: 'books',
119
+ id: newBook.id,
120
+ attributes: {
121
+ title: newBook.title,
122
+ author: newBook.author,
123
+ year: newBook.year
124
+ }
125
+ }
126
+ };
127
+ };
94
128
 
95
- ### Plugin System
129
+ api.vars.helpers.dataPut = async ({ scopeName, id, inputRecord }) => {
130
+ // Pretend to replace a book
131
+ return {
132
+ data: {
133
+ type: 'books',
134
+ id: id,
135
+ attributes: inputRecord.data.attributes
136
+ }
137
+ };
138
+ };
96
139
 
97
- Extend functionality through plugins:
140
+ api.vars.helpers.dataPatch = async ({ scopeName, id, inputRecord }) => {
141
+ // Pretend to update a book
142
+ return {
143
+ data: {
144
+ type: 'books',
145
+ id: id,
146
+ attributes: inputRecord.data.attributes
147
+ }
148
+ };
149
+ };
98
150
 
99
- ```javascript
100
- const api = new Api();
101
-
102
- api
103
- .use(ValidationPlugin)
104
- .use(MySQLPlugin, { connection: dbConfig })
105
- .use(HTTPPlugin, { basePath: '/api' })
106
- .use(PositioningPlugin)
107
- .use(VersioningPlugin, { trackHistory: true });
151
+ api.vars.helpers.dataDelete = async ({ scopeName, id }) => {
152
+ // Just return success
153
+ return { success: true };
154
+ };
108
155
  ```
109
156
 
110
- ### Custom Plugins
157
+ ### Using the API Programmatically
111
158
 
112
- Create your own plugins:
159
+ With just the REST API plugin, you can only use the API programmatically:
113
160
 
114
161
  ```javascript
115
- const MyPlugin = {
116
- install(api, options) {
117
- // Add new methods
118
- api.myMethod = () => { /* ... */ };
119
-
120
- // Register hooks
121
- api.hook('beforeInsert', async (context) => {
122
- // Modify context before insert
123
- });
124
-
125
- // Add custom types
126
- api.registerType('myType', ({ value }) => {
127
- // Custom type logic
128
- return processedValue;
129
- });
162
+ // Query all books
163
+ const allBooks = await api.resources.books.query({});
164
+ console.log(allBooks);
165
+ // Output: { data: [{ type: 'books', id: '1', attributes: {...} }, ...] }
166
+
167
+ // Get a single book
168
+ const book = await api.resources.books.get({ id: '1' });
169
+ console.log(book);
170
+ // Output: { data: { type: 'books', id: '1', attributes: {...} } }
171
+
172
+ // Create a new book
173
+ const newBook = await api.resources.books.post({
174
+ inputRecord: {
175
+ data: {
176
+ type: 'books',
177
+ attributes: {
178
+ title: 'New Book',
179
+ author: 'New Author',
180
+ year: 2024
181
+ }
182
+ }
130
183
  }
131
- };
184
+ });
185
+ console.log(newBook);
186
+
187
+ // Update a book (replace all fields)
188
+ const updatedBook = await api.resources.books.put({
189
+ id: '1',
190
+ inputRecord: {
191
+ data: {
192
+ type: 'books',
193
+ attributes: {
194
+ title: 'Updated Title',
195
+ author: 'Updated Author',
196
+ year: 2024
197
+ }
198
+ }
199
+ }
200
+ });
132
201
 
133
- api.use(MyPlugin, { /* options */ });
202
+ // Partially update a book
203
+ const patchedBook = await api.resources.books.patch({
204
+ id: '1',
205
+ inputRecord: {
206
+ data: {
207
+ type: 'books',
208
+ attributes: {
209
+ title: 'New Title Only'
210
+ }
211
+ }
212
+ }
213
+ });
214
+
215
+ // Delete a book
216
+ await api.resources.books.delete({ id: '1' });
134
217
  ```
135
218
 
136
- ## API Methods
219
+ The connector plugins allow you to expose the API in several ways. The core plugins expose the API to Express or to pure Node.
137
220
 
138
- ### CRUD Operations
221
+ ## Connector Plugins
139
222
 
140
- ```javascript
141
- // Get single resource
142
- const user = await api.get('123', { type: 'users' });
143
-
144
- // Query resources
145
- const users = await api.query({
146
- filter: { active: true },
147
- sort: '-createdAt,name',
148
- page: { size: 20, number: 1 }
149
- }, { type: 'users' });
150
-
151
- // Insert resource
152
- const newUser = await api.insert({
153
- name: 'John Doe',
154
- email: 'john@example.com'
155
- }, { type: 'users' });
156
-
157
- // Update resource
158
- const updated = await api.update('123', {
159
- name: 'Jane Doe'
160
- }, { type: 'users' });
161
-
162
- // Delete resource
163
- await api.delete('123', { type: 'users' });
164
- ```
223
+ While the REST API plugin provides the core functionality, you need a connector plugin to expose your API over HTTP. You can choose between Express or the pure HTTP plugin.
165
224
 
166
- ### HTTP Endpoints
225
+ > **Important**: Use either the Express plugin OR the HTTP plugin, not both!
167
226
 
168
- When using HTTPPlugin, the following endpoints are available:
227
+ ### Features (Common to Both Plugins)
169
228
 
170
- - `GET /api/{type}` - List resources
171
- - `GET /api/{type}/{id}` - Get single resource
172
- - `POST /api/{type}` - Create resource
173
- - `PATCH /api/{type}/{id}` - Update resource
174
- - `DELETE /api/{type}/{id}` - Delete resource
229
+ Both connector plugins provide these features:
175
230
 
176
- Query parameters:
177
- - `filter[field]=value` - Filter by field
178
- - `sort=-field1,field2` - Sort by fields
179
- - `page[size]=10&page[number]=2` - Pagination
180
- - `include=relation1,relation2` - Include relationships
231
+ - Automatic route creation for all resources (GET, POST, PUT, PATCH, DELETE)
232
+ - Full JSON:API compliant request/response handling
233
+ - Query parameter parsing (include, fields, filter, sort, page)
234
+ - Request body parsing with configurable size limits
235
+ - Strict content type validation with `strictContentType` option (default: true)
236
+ - Enforces `application/vnd.api+json` or `application/json` for POST/PUT/PATCH
237
+ - Returns 415 Unsupported Media Type for invalid content types
238
+ - Error responses in JSON:API format with proper HTTP status codes (400, 403, 404, 405, 409, 415, 422, 500)
239
+ - File upload support when used with FileHandlingPlugin
240
+ - Access to raw request/response objects for advanced use cases
241
+ - Dynamic resource support (resources added after server startup work automatically)
181
242
 
182
- ## Storage Backends
243
+ Example error response (same for both plugins):
244
+ ```json
245
+ {
246
+ "errors": [{
247
+ "status": "422",
248
+ "title": "Validation Error",
249
+ "detail": "Title is required",
250
+ "source": { "pointer": "/data/attributes/title" }
251
+ }]
252
+ }
253
+ ```
183
254
 
184
- ### Memory Storage
255
+ ### Express Plugin
185
256
 
186
- ```javascript
187
- api.use(MemoryPlugin, {
188
- initialData: [
189
- { id: 1, name: 'Item 1' },
190
- { id: 2, name: 'Item 2' }
191
- ]
192
- });
257
+ The Express plugin integrates your API with Express.js, the popular Node.js web framework. In addition to the common features above, it provides:
258
+
259
+ - Middleware injection support (before/after routes, per-resource)
260
+ - Works with any Express middleware ecosystem
261
+ - Familiar Express router patterns
262
+
263
+ First, install Express:
264
+
265
+ ```bash
266
+ npm install express
193
267
  ```
194
268
 
195
- ### MySQL Storage
269
+ Then update your code:
196
270
 
197
271
  ```javascript
198
- api.use(MySQLPlugin, {
199
- connection: {
200
- host: 'localhost',
201
- user: 'root',
202
- password: 'password',
203
- database: 'myapp'
204
- }
272
+ import { Api } from 'hooked-api';
273
+ import { RestApiPlugin, ExpressPlugin } from 'json-rest-api';
274
+ import express from 'express';
275
+
276
+ const api = new Api({
277
+ name: 'my-library-api',
278
+ version: '1.0.0'
205
279
  });
206
280
 
207
- // Sync schema with database
208
- await api.syncSchema(userSchema, 'users');
209
- ```
281
+ // Add plugins
282
+ api.use(RestApiPlugin);
283
+ api.use(ExpressPlugin, {
284
+ basePath: '/api', // Optional, defaults to '/api'
285
+ });
210
286
 
211
- ## Advanced Features
287
+ // Add your books resource and helpers here (same as before)
288
+ // ...
212
289
 
213
- ### Positioning
290
+ // Create Express app
291
+ const app = express();
214
292
 
215
- Manage record order:
293
+ // Mount the API routes - both approaches work:
216
294
 
217
- ```javascript
218
- api.use(PositioningPlugin);
219
-
220
- // Insert at specific position
221
- await api.insert({
222
- name: 'Item',
223
- beforeId: '123' // Place before item with ID 123
224
- }, {
225
- type: 'items',
226
- positioning: { enabled: true }
227
- });
295
+ // Approach 1: Direct router usage (standard Express pattern)
296
+ app.use(api.http.express.router); // Mount at root
297
+ // OR
298
+ app.use('/v1', api.http.express.router); // Mount at specific path
228
299
 
229
- // Reposition existing item
230
- await api.reposition('items', '456', null); // Move to end
300
+ // Approach 2: Convenience method (adds logging)
301
+ api.http.express.mount(app); // Mount at root
302
+ // OR
303
+ api.http.express.mount(app, '/v1'); // Mount at specific path
304
+
305
+ // Note: The mount() method is just syntactic sugar that calls app.use() and logs the mount path
306
+
307
+ // Start the server
308
+ app.listen(3000, () => {
309
+ console.log('API running at http://localhost:3000/api');
310
+ });
231
311
  ```
232
312
 
233
- ### API Versioning
313
+ Now your API is ready to receive HTTP requests!
314
+
315
+ ### HTTP Plugin
316
+
317
+ If you prefer not to use Express, the HTTP plugin provides a lightweight alternative using only Node.js built-in modules. In addition to the common features above, it provides:
234
318
 
235
- Create multiple versions of your API with automatic version negotiation:
319
+ - Zero framework dependencies (pure Node.js)
320
+ - Built-in HTTP server with configurable port
321
+ - Multiple deployment options (standalone server, HTTPS with custom handler, integration with existing servers)
322
+ - Character encoding support (respects Content-Type charset)
323
+
324
+ #### Configuration Options
236
325
 
237
326
  ```javascript
238
- // Version 1.0.0
239
- const apiV1 = createApi({
240
- name: 'users',
241
- version: '1.0.0',
242
- storage: 'memory'
327
+ import { Api } from 'hooked-api';
328
+ import { RestApiPlugin, HttpPlugin } from 'json-rest-api';
329
+
330
+ const api = new Api({
331
+ name: 'my-library-api',
332
+ version: '1.0.0'
243
333
  });
244
334
 
245
- apiV1.addResource('users', new Schema({
246
- id: { type: 'id' },
247
- name: { type: 'string', required: true },
248
- email: { type: 'string', required: true }
249
- }));
250
-
251
- // Version 2.0.0 with new fields
252
- const apiV2 = createApi({
253
- name: 'users',
254
- version: '2.0.0',
255
- storage: 'memory'
335
+ // Add plugins
336
+ api.use(RestApiPlugin);
337
+ api.use(HttpPlugin, {
338
+ port: 3000, // Server port (default: 3000)
339
+ basePath: '/api', // API base path (default: '/api')
340
+ strictContentType: true, // Enforce JSON content types (default: true)
341
+ requestSizeLimit: '10mb' // Max request body size (default: '1mb')
256
342
  });
257
343
 
258
- apiV2.addResource('users', new Schema({
259
- id: { type: 'id' },
260
- name: { type: 'string', required: true },
261
- email: { type: 'string', required: true },
262
- phone: { type: 'string' }, // New field
263
- active: { type: 'boolean', default: true } // New field
264
- }));
265
-
266
- // Automatic version selection
267
- const api = Api.get('users', 'latest'); // Gets v2.0.0
268
- const apiV1Compat = Api.get('users', '1.0.0'); // Gets v1.0.0 or higher
269
-
270
- // HTTP endpoints with automatic versioning
271
- // GET /api/1.0.0/users
272
- // GET /api/2.0.0/users
273
- // GET /api/users?v=2.0.0
274
- // GET /api/users (with API-Version: 2.0.0 header)
275
- ```
344
+ // Add your books resource and helpers here (same as before)
345
+ // ...
276
346
 
277
- ### Resource Versioning
347
+ // Start the HTTP server - multiple options:
278
348
 
279
- Track changes to individual resources:
349
+ // Option 1: Start on configured port (3000 by default)
350
+ api.http.startServer();
280
351
 
281
- ```javascript
282
- api.use(VersioningPlugin, {
283
- trackHistory: true,
284
- optimisticLocking: true
352
+ // Option 2: Start on a different port
353
+ api.http.startServer(4000);
354
+
355
+ // Option 3: Access the raw server for custom configuration
356
+ const server = api.http.server;
357
+ server.timeout = 60000; // 60 second timeout
358
+ server.listen(3000);
359
+
360
+ // Option 4: Use with HTTPS
361
+ import { createServer } from 'https';
362
+ const httpsServer = createServer(sslOptions, api.http.handler);
363
+ httpsServer.listen(443);
364
+
365
+ // Option 5: Integrate into existing server
366
+ myExistingServer.on('request', (req, res) => {
367
+ if (req.url.startsWith('/api')) {
368
+ api.http.handler(req, res);
369
+ } else {
370
+ // Handle other routes
371
+ }
285
372
  });
286
373
 
287
- // Automatic version tracking
288
- const user = await api.insert({ name: 'John' }, { type: 'users' });
289
- // user.version = 1
374
+ console.log('API running at http://localhost:3000/api');
375
+ ```
290
376
 
291
- const updated = await api.update(user.id, { name: 'Jane' }, { type: 'users' });
292
- // updated.version = 2
293
377
 
294
- // Get version history
295
- const history = await api.getVersionHistory('users', user.id);
378
+ #### Character Encoding
296
379
 
297
- // Restore previous version
298
- await api.restoreVersion('users', user.id, 1);
380
+ The plugin respects the charset parameter in Content-Type headers:
299
381
 
300
- // Compare versions
301
- const diff = await api.diffVersions('users', user.id, 1, 2);
382
+ ```bash
383
+ curl -X POST http://localhost:3000/api/books \
384
+ -H "Content-Type: application/json; charset=utf-16" \
385
+ -d '...'
302
386
  ```
303
387
 
304
- ### Hooks
388
+ The HTTP plugin provides the same REST endpoints as Express but with zero external framework dependencies.
305
389
 
306
- Extend behavior with hooks:
390
+ ### Using Your REST API
307
391
 
308
- ```javascript
309
- // Validation hooks
310
- api.hook('beforeValidate', async (context) => {
311
- // Modify data before validation
312
- });
392
+ Once you've set up either the Express or HTTP plugin, you can interact with your API using any HTTP client. Here are examples using curl that show the **full JSON:API compliant responses** you'll receive:
313
393
 
314
- api.hook('afterValidate', async (context) => {
315
- // Add custom validation
316
- if (context.data.price < 0) {
317
- context.errors.push({
318
- field: 'price',
319
- message: 'Price cannot be negative'
320
- });
394
+ ```bash
395
+ # Get all books
396
+ curl http://localhost:3000/api/books
397
+
398
+ # Returns:
399
+ {
400
+ "data": [
401
+ {
402
+ "type": "books",
403
+ "id": "1",
404
+ "attributes": {
405
+ "title": "The Great Gatsby",
406
+ "author": "F. Scott Fitzgerald",
407
+ "year": 1925
408
+ }
409
+ },
410
+ {
411
+ "type": "books",
412
+ "id": "2",
413
+ "attributes": {
414
+ "title": "1984",
415
+ "author": "George Orwell",
416
+ "year": 1949
417
+ }
418
+ }
419
+ ]
420
+ }
421
+
422
+ # Get a single book
423
+ curl http://localhost:3000/api/books/1
424
+
425
+ # Returns:
426
+ {
427
+ "data": {
428
+ "type": "books",
429
+ "id": "1",
430
+ "attributes": {
431
+ "title": "The Great Gatsby",
432
+ "author": "F. Scott Fitzgerald",
433
+ "year": 1925
434
+ }
435
+ }
436
+ }
437
+
438
+ # Create a new book
439
+ curl -X POST http://localhost:3000/api/books \
440
+ -H "Content-Type: application/json" \
441
+ -d '{
442
+ "data": {
443
+ "type": "books",
444
+ "attributes": {
445
+ "title": "New Book",
446
+ "author": "New Author",
447
+ "year": 2024
448
+ }
449
+ }
450
+ }'
451
+
452
+ # Returns (201 Created):
453
+ {
454
+ "data": {
455
+ "type": "books",
456
+ "id": "1234567890",
457
+ "attributes": {
458
+ "title": "New Book",
459
+ "author": "New Author",
460
+ "year": 2024
461
+ }
462
+ }
463
+ }
464
+
465
+ # Update a book
466
+ curl -X PATCH http://localhost:3000/api/books/1 \
467
+ -H "Content-Type: application/json" \
468
+ -d '{
469
+ "data": {
470
+ "type": "books",
471
+ "attributes": {
472
+ "title": "Updated Title"
473
+ }
474
+ }
475
+ }'
476
+
477
+ # Returns:
478
+ {
479
+ "data": {
480
+ "type": "books",
481
+ "id": "1",
482
+ "attributes": {
483
+ "title": "Updated Title",
484
+ "author": "F. Scott Fitzgerald",
485
+ "year": 1925
486
+ }
321
487
  }
488
+ }
489
+
490
+ # Delete a book
491
+ curl -X DELETE http://localhost:3000/api/books/1
492
+
493
+ # Returns: 204 No Content (empty response)
494
+ ```
495
+
496
+ ### Database Integration with Knex
497
+
498
+ While these examples use fake data helpers for demonstration, jsonrestapi includes a basic Knex plugin for database integration:
499
+
500
+ ```javascript
501
+ import { Api } from 'hooked-api';
502
+ import { RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api';
503
+ import knex from 'knex';
504
+
505
+ // Create your Knex instance
506
+ const db = knex({
507
+ client: 'postgres',
508
+ connection: process.env.DATABASE_URL
322
509
  });
323
510
 
324
- // CRUD hooks
325
- api.hook('beforeInsert', async (context) => {
326
- context.data.createdBy = context.options.userId;
511
+ // Use the Knex plugin
512
+ api.use(RestApiPlugin);
513
+ api.use(RestApiKnexPlugin, {
514
+ knex: { knex: db }
327
515
  });
328
516
 
329
- api.hook('transformResult', async (context) => {
330
- // Add computed fields
331
- if (context.result) {
332
- context.result.displayName = context.result.name.toUpperCase();
333
- }
517
+ // Now all your resources automatically use the database!
518
+ api.addResource('books', {
519
+ schema: { /* ... */ }
334
520
  });
335
521
  ```
336
522
 
337
- ## Schema Extensions
523
+ The basic Knex plugin provides simple CRUD operations. More advanced database plugins (coming soon) will add filtering, sorting, pagination, and relationships out of the box!
338
524
 
339
- Add custom types and parameters:
340
525
 
526
+ ### File Upload Support in Connector Plugins
527
+
528
+ Both the Express and HTTP plugins provide support for the FileHandlingPlugin by:
529
+
530
+ 1. **Providing raw request/response access**: The plugins add `_httpReq` and `_httpRes` parameters that the FileHandlingPlugin uses to detect and parse multipart uploads.
531
+
532
+ 2. **Registering file detectors**: When file uploads are enabled, the connector plugins register appropriate file detectors with the REST API.
533
+
534
+ #### Configuration
535
+
536
+ ##### Express Plugin
341
537
  ```javascript
342
- // Custom type
343
- schema.registerType('phone', ({ value }) => {
344
- const cleaned = value.replace(/\D/g, '');
345
- if (cleaned.length !== 10) {
346
- throw new Error('Invalid phone number');
538
+ api.use(ExpressPlugin, {
539
+ enableFileUploads: true, // Enable file upload support (default: true)
540
+ fileParser: 'busboy', // Parser: 'busboy' or 'formidable'
541
+ fileParserOptions: { // Options passed to the file parser
542
+ limits: {
543
+ fileSize: 10 * 1024 * 1024, // 10MB file size limit
544
+ files: 5 // Max 5 files per request
545
+ }
347
546
  }
348
- return cleaned;
349
547
  });
548
+ ```
350
549
 
351
- // Custom parameter
352
- schema.registerParam('unique', async ({ value, fieldName, object }) => {
353
- const exists = await checkUniqueness(fieldName, value);
354
- if (exists) {
355
- throw new Error(`${fieldName} must be unique`);
550
+ ##### HTTP Plugin
551
+ ```javascript
552
+ api.use(HttpPlugin, {
553
+ enableFileUploads: true, // Enable file upload support (default: true)
554
+ fileParser: 'busboy', // Parser: 'busboy' or 'formidable'
555
+ fileParserOptions: { // Options passed to the file parser
556
+ limits: {
557
+ fileSize: 10 * 1024 * 1024, // 10MB file size limit
558
+ files: 5 // Max 5 files per request
559
+ }
356
560
  }
357
561
  });
358
-
359
- // Use in schema
360
- const schema = new Schema({
361
- phone: { type: 'phone', required: true },
362
- username: { type: 'string', unique: true }
363
- });
364
562
  ```
365
563
 
366
- ## Cross-API Communication
564
+ #### File Parsers
367
565
 
368
- APIs can access each other with automatic version compatibility:
566
+ Both plugins support the same file parsers:
567
+
568
+ - **busboy**: Streaming parser, memory efficient, good for large files
569
+ - **formidable**: Saves to temp files, includes progress tracking
570
+
571
+ **Note**: File parsing requires the FileHandlingPlugin to be loaded. The connector plugins only provide the detection and parsing capabilities; the actual file handling logic is implemented by the FileHandlingPlugin.
572
+
573
+ ## File Storage
574
+
575
+ The file handling system allows you to accept file uploads in your API. It consists of three parts:
576
+
577
+ 1. The `FileHandlingPlugin` - orchestrates file processing
578
+ 2. File detectors - parse multipart uploads (built into connector plugins)
579
+ 3. Storage adapters - save files to disk or cloud storage
580
+
581
+ ### Setting Up File Uploads
582
+
583
+ First, let's add file support to our books API. We'll allow users to upload book covers:
369
584
 
370
585
  ```javascript
371
- // Orders API can access Users API
372
- const ordersApi = createApi({
373
- name: 'orders',
586
+ import { Api } from 'hooked-api';
587
+ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
588
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
589
+ import express from 'express';
590
+
591
+ const api = new Api({
592
+ name: 'my-library-api',
374
593
  version: '1.0.0'
375
594
  });
376
595
 
377
- ordersApi.hook('afterInsert', async (context) => {
378
- // Automatically gets a compatible users API
379
- const usersApi = ordersApi.apis.users;
380
-
381
- if (usersApi && context.data.userId) {
382
- const user = await usersApi.get(context.data.userId, { type: 'users' });
383
- console.log('Order created for:', user);
596
+ // Create a storage adapter
597
+ const coverStorage = new LocalStorage({
598
+ directory: './uploads/covers',
599
+ baseUrl: 'http://localhost:3000/uploads/covers'
600
+ });
601
+
602
+ // Add plugins (ORDER MATTERS!)
603
+ api.use(RestApiPlugin);
604
+ api.use(FileHandlingPlugin); // Must come after RestApiPlugin
605
+ api.use(ExpressPlugin); // Must come after FileHandlingPlugin
606
+
607
+ // Update the books schema to include a cover image
608
+ api.addResource('books', {
609
+ schema: {
610
+ title: { type: 'string', required: true },
611
+ author: { type: 'string', required: true },
612
+ year: { type: 'number' },
613
+ isbn: { type: 'string' },
614
+ cover: {
615
+ type: 'file',
616
+ storage: coverStorage,
617
+ accepts: ['image/jpeg', 'image/png', 'image/gif'],
618
+ maxSize: '5mb'
619
+ }
384
620
  }
385
621
  });
622
+
623
+ // Update dataPost to handle the cover URL
624
+ api.vars.helpers.dataPost = async ({ scopeName, inputRecord }) => {
625
+ const newBook = {
626
+ id: String(Date.now()),
627
+ ...inputRecord.data.attributes
628
+ };
629
+
630
+ // The file has already been uploaded and replaced with a URL
631
+ console.log('Book cover URL:', newBook.cover);
632
+
633
+ return {
634
+ data: {
635
+ type: 'books',
636
+ id: newBook.id,
637
+ attributes: {
638
+ title: newBook.title,
639
+ author: newBook.author,
640
+ year: newBook.year,
641
+ cover: newBook.cover // This is now a URL!
642
+ }
643
+ }
644
+ };
645
+ };
646
+
647
+ // Add other helpers...
648
+
649
+ // Create Express app
650
+ const app = express();
651
+
652
+ // Serve uploaded files
653
+ app.use('/uploads', express.static('./uploads'));
654
+
655
+ // Mount the API (both approaches work)
656
+ app.use(api.express.router); // Direct Express approach
657
+ // OR
658
+ api.express.mount(app); // Convenience method
659
+
660
+ app.listen(3000, () => {
661
+ console.log('API with file uploads running at http://localhost:3000/api');
662
+ });
663
+ ```
664
+
665
+ ### File Uploads with Express
666
+
667
+ To upload a file, you need to send a multipart/form-data request. Here's an example HTML form:
668
+
669
+ ```html
670
+ <!DOCTYPE html>
671
+ <html>
672
+ <body>
673
+ <h2>Add a Book</h2>
674
+ <form action="http://localhost:3000/api/books" method="POST" enctype="multipart/form-data">
675
+ <label>Title: <input name="title" required></label><br>
676
+ <label>Author: <input name="author" required></label><br>
677
+ <label>Year: <input name="year" type="number"></label><br>
678
+ <label>Cover: <input name="cover" type="file" accept="image/*"></label><br>
679
+ <button type="submit">Add Book</button>
680
+ </form>
681
+ </body>
682
+ </html>
683
+ ```
684
+
685
+ Or using curl:
686
+
687
+ ```bash
688
+ curl -X POST http://localhost:3000/api/books \
689
+ -F "title=The Hobbit" \
690
+ -F "author=J.R.R. Tolkien" \
691
+ -F "year=1937" \
692
+ -F "cover=@/path/to/cover.jpg"
386
693
  ```
387
694
 
388
- ## Security Best Practices
695
+ The file handling plugin will:
696
+ 1. Detect the multipart upload
697
+ 2. Parse the files using busboy (you'll need to install it: `npm install busboy`)
698
+ 3. Validate file type and size according to your schema
699
+ 4. Upload the file using the storage adapter
700
+ 5. Replace the file field with the uploaded file's URL
701
+ 6. Convert everything to JSON:API format
702
+
703
+ ### Storage Adapters
389
704
 
390
- Enable comprehensive security features:
705
+ #### LocalStorage
706
+
707
+ The LocalStorage adapter saves files to your local filesystem:
391
708
 
392
709
  ```javascript
393
- import { SecurityPlugin, LoggingPlugin } from 'jsonrestapi';
394
-
395
- api
396
- .use(SecurityPlugin, {
397
- rateLimit: { max: 100, windowMs: 15 * 60 * 1000 },
398
- authentication: { type: 'bearer', required: true },
399
- cors: { origin: 'https://app.example.com' }
400
- })
401
- .use(LoggingPlugin, {
402
- level: 'info',
403
- auditLog: true
404
- });
710
+ import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
711
+
712
+ // Basic usage
713
+ const storage = new LocalStorage({
714
+ directory: './uploads',
715
+ baseUrl: 'http://localhost:3000/uploads'
716
+ });
717
+
718
+ // With security options
719
+ const secureStorage = new LocalStorage({
720
+ directory: './uploads',
721
+ baseUrl: 'http://localhost:3000/uploads',
722
+ nameStrategy: 'hash', // Generate random filenames
723
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf'],
724
+ maxFilenameLength: 255
725
+ });
726
+
727
+ // Different naming strategies
728
+ const customStorage = new LocalStorage({
729
+ directory: './uploads',
730
+ baseUrl: 'http://localhost:3000/uploads',
731
+ nameStrategy: 'custom',
732
+ nameGenerator: async (file) => {
733
+ // Generate your own filename
734
+ const timestamp = Date.now();
735
+ const random = Math.random().toString(36).substring(7);
736
+ return `book_cover_${timestamp}_${random}`;
737
+ }
738
+ });
405
739
  ```
406
740
 
407
- ## Error Handling
741
+ #### S3Storage (Mock Implementation)
408
742
 
409
- Errors follow JSON:API format:
743
+ The package includes a mock S3 storage adapter for development:
410
744
 
411
745
  ```javascript
412
- try {
413
- await api.insert({ name: '' }, { type: 'users' });
414
- } catch (error) {
415
- console.log(error.errors);
416
- // [{
417
- // status: '422',
418
- // title: 'Validation Error',
419
- // detail: 'Field required',
420
- // source: { pointer: '/data/attributes/name' }
421
- // }]
422
- }
746
+ import { S3Storage } from 'jsonrestapi/plugins/storage/s3-storage.js';
747
+
748
+ const s3Storage = new S3Storage({
749
+ bucket: 'my-book-covers',
750
+ region: 'us-east-1',
751
+ prefix: 'covers/',
752
+ mockMode: true // Currently only mock mode is implemented
753
+ });
423
754
  ```
424
755
 
425
- ## License
756
+ > **Looking for a complete example with multiple files?** Check out our [Complete File Upload Guide](docs/mini-guides/file-uploads-complete.md) that shows multiple file fields, HTML forms, and static file serving.
757
+
758
+ ## Next Steps
759
+
760
+ This tutorial covered the basics of creating a REST API with jsonrestapi:
761
+
762
+ 1. The REST API plugin for core functionality
763
+ 2. Connector plugins for HTTP access (Express or pure HTTP)
764
+ 3. File handling with storage adapters
765
+
766
+ In future sections, we'll cover:
767
+ - Database integration for persistent storage
768
+ - Advanced querying with filters, sorting, and pagination
769
+ - Relationships between resources
770
+ - Authentication and authorization
771
+ - Custom validation and business logic
426
772
 
427
- MIT
773
+ For now, you have everything you need to create a basic REST API with file upload support!