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.
- package/.claude/settings.local.json +16 -0
- package/LICENSE +674 -20
- package/README.md +728 -120
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +28 -202
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +37 -20
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
package/README.md
CHANGED
|
@@ -1,165 +1,773 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
## Table of Contents
|
|
12
6
|
|
|
13
|
-
|
|
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
|
-
|
|
16
|
+
## Getting Started
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
First, let's install the required packages:
|
|
18
19
|
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
```javascript
|
|
43
|
+
import { Api } from 'hooked-api';
|
|
44
|
+
import { RestApiPlugin } from 'json-rest-api';
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
+
### Creating a Books Resource
|
|
56
|
+
|
|
57
|
+
Now let's create a simple "books" resource with fake data:
|
|
50
58
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
255
|
+
### Express Plugin
|
|
82
256
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
326
|
+
```javascript
|
|
327
|
+
import { Api } from 'hooked-api';
|
|
328
|
+
import { RestApiPlugin, HttpPlugin } from 'json-rest-api';
|
|
107
329
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
349
|
+
// Option 1: Start on configured port (3000 by default)
|
|
350
|
+
api.http.startServer();
|
|
113
351
|
|
|
114
|
-
|
|
115
|
-
|
|
352
|
+
// Option 2: Start on a different port
|
|
353
|
+
api.http.startServer(4000);
|
|
116
354
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
758
|
+
## Next Steps
|
|
148
759
|
|
|
149
|
-
|
|
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
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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!
|