json-rest-api 0.1.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -20
  3. package/README.md +728 -120
  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 +28 -202
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +37 -20
  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/.jshintrc +0 -48
  46. package/.npmignore +0 -15
  47. package/examples/client.js +0 -18
  48. package/examples/client2.js +0 -27
  49. package/examples/server.js +0 -23
  50. package/test.js +0 -101
package/README.md CHANGED
@@ -1,165 +1,773 @@
1
- json-rest-api
2
- =============
3
- A lightweight REST API that receives and responds to JSON HTTP requests,
4
- supports all verbs. Expects JSON requests and responds in JSON via an added
5
- method to the http.ServerResponse object called `json` (see below).
1
+ # JSON REST API Tutorial
6
2
 
7
- # Motivation
8
- Sometimes you want a simple REST API and don't need all the bells and whistles
9
- of express.js.
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.
10
4
 
11
- # Installation
5
+ ## Table of Contents
12
6
 
13
- npm install json-rest-api
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)
14
15
 
15
- # Example
16
+ ## Getting Started
16
17
 
17
- ## server
18
+ First, let's install the required packages:
18
19
 
19
- ```JavaScript
20
- 'use strict';
21
- var JsonRestApi = require('json-rest-api');
22
- var inspect = require('util').inspect;
23
- var debug = require('debug')('json-rest-api:example:server');
20
+ ```bash
21
+ npm install hooked-api jsonrestapi
22
+ ```
24
23
 
25
- var RestApi = new JsonRestApi({port: 8000}, function(err) {
26
- if (err) {
27
- debug('/ping error: '+inspect(err));
28
- return;
29
- }
30
- console.log('Listening on port 8000.');
24
+ Create a new file called `api.js`:
25
+
26
+ ```javascript
27
+ import { Api } from 'hooked-api';
28
+
29
+ // Create a new API instance
30
+ const api = new Api({
31
+ name: 'my-library-api',
32
+ version: '1.0.0'
33
+ });
34
+ ```
35
+
36
+ ## Basic REST API Plugin
37
+
38
+ The `RestApiPlugin` is the foundation of jsonrestapi. It adds REST methods (query, get, post, put, patch, delete) to your API scopes.
31
39
 
32
- // add a "GET" route
33
- RestApi.addRoute('get', '/ping', function(req, res, json, qs) {
34
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
35
- assert.deepEqual(qsExp, qs);
36
- res.json({success: true, pong: 'pong'});
37
- });
40
+ ### Adding the REST API Plugin
38
41
 
39
- // add a "POST" route
40
- RestApi.addRoute('get', '/setName', function(req, res, json, qs) {
41
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
42
+ ```javascript
43
+ import { Api } from 'hooked-api';
44
+ import { RestApiPlugin } from 'json-rest-api';
42
45
 
43
- assert.deepEqual(qsExp, qs);
44
- res.json({success: true, pong: 'pong'});
45
- });
46
+ const api = new Api({
47
+ name: 'my-library-api',
48
+ version: '1.0.0'
46
49
  });
50
+
51
+ // Use the REST API plugin
52
+ api.use(RestApiPlugin);
47
53
  ```
48
54
 
49
- ## client
55
+ ### Creating a Books Resource
56
+
57
+ Now let's create a simple "books" resource with fake data:
50
58
 
51
- ```JavaScript
52
- 'use strict';
53
- var request = require('request');
54
- var inspect = require('util').inspect;
55
- var debug = require('debug')('json-rest-api:example:client');
56
- var assert = require('assert');
59
+ ```javascript
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
+ ];
78
+
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 };
95
+
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
+ };
57
108
 
58
- var qs = {
59
- alpha: 'one',
60
- beta: 2,
61
- gamma: true
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
+ };
115
+
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
+ };
62
127
  };
63
128
 
64
- request({json:true, qs:qs, uri: 'http://localhost:8000/ping'}, function(err, res, json) {
65
- if (err) {
66
- debug('Error: '+inspect(err));
67
- return;
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
68
136
  }
137
+ };
138
+ };
69
139
 
70
- assert.ok(json.success === true);
71
- assert.ok(json.pong === 'pong');
72
- console.log('Success!');
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
+ };
150
+
151
+ api.vars.helpers.dataDelete = async ({ scopeName, id }) => {
152
+ // Just return success
153
+ return { success: true };
154
+ };
155
+ ```
156
+
157
+ ### Using the API Programmatically
158
+
159
+ With just the REST API plugin, you can only use the API programmatically:
160
+
161
+ ```javascript
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
+ }
183
+ }
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
+ }
73
200
  });
74
201
 
75
- request({json:true, qs:qs, uri: 'http://localhost:8000/setName'}, function(err, res, json) {
76
- if (err) {
77
- debug('Error: '+inspect(err));
78
- return;
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
+ }
79
211
  }
212
+ }
213
+ });
214
+
215
+ // Delete a book
216
+ await api.resources.books.delete({ id: '1' });
217
+ ```
218
+
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.
220
+
221
+ ## Connector Plugins
222
+
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.
224
+
225
+ > **Important**: Use either the Express plugin OR the HTTP plugin, not both!
226
+
227
+ ### Features (Common to Both Plugins)
228
+
229
+ Both connector plugins provide these features:
230
+
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)
242
+
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
+ ```
80
254
 
81
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
255
+ ### Express Plugin
82
256
 
83
- assert.ok(json.success === true);
84
- assert.ok(json.cmd === 'setName');
85
- assert.deepEqual(json.qs, qsExp);
86
- console.log('Success!');
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
267
+ ```
268
+
269
+ Then update your code:
270
+
271
+ ```javascript
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'
279
+ });
280
+
281
+ // Add plugins
282
+ api.use(RestApiPlugin);
283
+ api.use(ExpressPlugin, {
284
+ basePath: '/api', // Optional, defaults to '/api'
285
+ });
286
+
287
+ // Add your books resource and helpers here (same as before)
288
+ // ...
289
+
290
+ // Create Express app
291
+ const app = express();
292
+
293
+ // Mount the API routes - both approaches work:
294
+
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
299
+
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');
87
310
  });
88
311
  ```
89
312
 
90
- # API
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:
91
318
 
92
- ## RestApi([config], [cb])
93
- RestApi constructor. Creates the HTTP server objects and starts listening on
94
- the socket.
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)
95
323
 
96
- ### Params:
97
- * **Object** *[config]* Optional configuration object to configure the server.
98
- The configuration options are:
99
- * bindTo - Optional. The address to bind, default: INADDR_ANY.
100
- * port - Optional. The port to bind the server, default: 44401.
101
- * **Function** *[cb]* An optional callback. If there is a callback, the process
102
- will be begin listening on the socket for HTTP requests. Without a callback,
103
- you must call listen explicitly.
324
+ #### Configuration Options
104
325
 
105
- ## listen([cb])
106
- Start listening on the socket for HTTP requests.
326
+ ```javascript
327
+ import { Api } from 'hooked-api';
328
+ import { RestApiPlugin, HttpPlugin } from 'json-rest-api';
107
329
 
108
- ### Params:
109
- * **Function** *[cb]* An optional callback called when the server is ready.
330
+ const api = new Api({
331
+ name: 'my-library-api',
332
+ version: '1.0.0'
333
+ });
334
+
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')
342
+ });
343
+
344
+ // Add your books resource and helpers here (same as before)
345
+ // ...
346
+
347
+ // Start the HTTP server - multiple options:
110
348
 
111
- ## stop([cb])
112
- Stops the server from acccepting new connections and listening on the port.
349
+ // Option 1: Start on configured port (3000 by default)
350
+ api.http.startServer();
113
351
 
114
- ### Params:
115
- * **Function** *[cb]* The callback function. Optional.
352
+ // Option 2: Start on a different port
353
+ api.http.startServer(4000);
116
354
 
117
- ## addRoute(verb, path, cb)
118
- Add a route along with a function to run.
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
+ }
372
+ });
373
+
374
+ console.log('API running at http://localhost:3000/api');
375
+ ```
119
376
 
120
- ### Params:
121
- * **String** *verb* HTTP verb for route. e.g.: 'get', 'post'.
122
- * **String** *path* A valid URI path.
123
- * **Function** *cb* A function to run when the path executes. Which has the
124
- following form: fn(req, res, json, qs):
125
- * @param {Object} req - Node.js HTTP request object
126
- * @param {Object} res - Node.js HTTP response object
127
- * @param {Object} json - The JSON body of the request, if present.
128
- * @param {Object} qs - The query string of the request, if present.
129
377
 
130
- ### Return:
131
- * **Boolean|Error** True, if no errors on the parameters, Error
132
- * **Error|Boolean** true on success and Error on failure.
378
+ #### Character Encoding
133
379
 
134
- ## json([status code,] jsonObject)
135
- A method for the JsonServerResponse prototype to send JSON by passing an object.
380
+ The plugin respects the charset parameter in Content-Type headers:
381
+
382
+ ```bash
383
+ curl -X POST http://localhost:3000/api/books \
384
+ -H "Content-Type: application/json; charset=utf-16" \
385
+ -d '...'
386
+ ```
387
+
388
+ The HTTP plugin provides the same REST endpoints as Express but with zero external framework dependencies.
389
+
390
+ ### Using Your REST API
391
+
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:
393
+
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
+ }
487
+ }
488
+ }
136
489
 
137
- ### Params:
138
- * **Number|Object** *Optional.* First argument may be either the status code or
139
- the object to convert to JSON.
140
- * **Object** *Optional.* If the first object is the status code, then the second
141
- param must be the object to convert to JSON.
490
+ # Delete a book
491
+ curl -X DELETE http://localhost:3000/api/books/1
142
492
 
143
- # LICENSE
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
509
+ });
510
+
511
+ // Use the Knex plugin
512
+ api.use(RestApiPlugin);
513
+ api.use(RestApiKnexPlugin, {
514
+ knex: { knex: db }
515
+ });
516
+
517
+ // Now all your resources automatically use the database!
518
+ api.addResource('books', {
519
+ schema: { /* ... */ }
520
+ });
521
+ ```
522
+
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!
524
+
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
537
+ ```javascript
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
+ }
546
+ }
547
+ });
548
+ ```
549
+
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
+ }
560
+ }
561
+ });
562
+ ```
563
+
564
+ #### File Parsers
565
+
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:
584
+
585
+ ```javascript
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',
593
+ version: '1.0.0'
594
+ });
595
+
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
+ }
620
+ }
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"
693
+ ```
694
+
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
704
+
705
+ #### LocalStorage
706
+
707
+ The LocalStorage adapter saves files to your local filesystem:
708
+
709
+ ```javascript
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
+ });
739
+ ```
740
+
741
+ #### S3Storage (Mock Implementation)
742
+
743
+ The package includes a mock S3 storage adapter for development:
744
+
745
+ ```javascript
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
+ });
754
+ ```
144
755
 
145
- The MIT License (MIT)
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.
146
757
 
147
- Copyright (c) 2013,2014 Edmond Meinfelder
758
+ ## Next Steps
148
759
 
149
- Permission is hereby granted, free of charge, to any person obtaining a copy of
150
- this software and associated documentation files (the "Software"), to deal in
151
- the Software without restriction, including without limitation the rights to
152
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
153
- the Software, and to permit persons to whom the Software is furnished to do so,
154
- subject to the following conditions:
760
+ This tutorial covered the basics of creating a REST API with jsonrestapi:
155
761
 
156
- The above copyright notice and this permission notice shall be included in all
157
- copies or substantial portions of the Software.
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
158
765
 
159
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
160
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
161
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
162
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
163
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
164
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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
165
772
 
773
+ For now, you have everything you need to create a basic REST API with file upload support!