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.
- package/.claude/settings.local.json +16 -0
- package/LICENSE +674 -0
- package/README.md +655 -309
- 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 +31 -56
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +41 -14
- 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/TODO.todo +0 -113
- package/api.js +0 -628
- package/docs/API.md +0 -1306
- package/docs/FIXES_APPLIED.md +0 -59
- package/docs/GUIDES.md +0 -1792
- package/docs/RESOURCES_IN_EXPRESS.md +0 -456
- package/docs/example-versioning.js +0 -168
- package/docs/example.js +0 -195
- package/plugins/API guide.md +0 -364
- package/plugins/api-registry.js +0 -142
- package/plugins/api-versioning.md +0 -135
- package/plugins/http.js +0 -273
- package/plugins/logging.js +0 -255
- package/plugins/memory.js +0 -182
- package/plugins/mysql.js +0 -753
- package/plugins/openapi.js +0 -401
- package/plugins/positioning.js +0 -220
- package/plugins/security.js +0 -261
- package/plugins/validation.js +0 -173
- package/plugins/versioning.js +0 -288
- package/resource-helper.js +0 -79
- package/schema.js +0 -481
- 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
|
-
|
|
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
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
24
|
+
Create a new file called `api.js`:
|
|
28
25
|
|
|
29
26
|
```javascript
|
|
30
|
-
import {
|
|
31
|
-
import express from 'express';
|
|
27
|
+
import { Api } from 'hooked-api';
|
|
32
28
|
|
|
33
|
-
// Create
|
|
34
|
-
const api =
|
|
35
|
-
name: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
const app = express();
|
|
54
|
-
api.mount(app);
|
|
40
|
+
### Adding the REST API Plugin
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
```javascript
|
|
43
|
+
import { Api } from 'hooked-api';
|
|
44
|
+
import { RestApiPlugin } from 'json-rest-api';
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
const api = new Api({
|
|
47
|
+
name: 'my-library-api',
|
|
48
|
+
version: '1.0.0'
|
|
49
|
+
});
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
// Use the REST API plugin
|
|
52
|
+
api.use(RestApiPlugin);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Creating a Books Resource
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
Now let's create a simple "books" resource with fake data:
|
|
64
58
|
|
|
65
59
|
```javascript
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
###
|
|
157
|
+
### Using the API Programmatically
|
|
111
158
|
|
|
112
|
-
|
|
159
|
+
With just the REST API plugin, you can only use the API programmatically:
|
|
113
160
|
|
|
114
161
|
```javascript
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
+
## Connector Plugins
|
|
139
222
|
|
|
140
|
-
|
|
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
|
-
|
|
225
|
+
> **Important**: Use either the Express plugin OR the HTTP plugin, not both!
|
|
167
226
|
|
|
168
|
-
|
|
227
|
+
### Features (Common to Both Plugins)
|
|
169
228
|
|
|
170
|
-
|
|
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
|
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
- `
|
|
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
|
-
|
|
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
|
-
###
|
|
255
|
+
### Express Plugin
|
|
185
256
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
269
|
+
Then update your code:
|
|
196
270
|
|
|
197
271
|
```javascript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
281
|
+
// Add plugins
|
|
282
|
+
api.use(RestApiPlugin);
|
|
283
|
+
api.use(ExpressPlugin, {
|
|
284
|
+
basePath: '/api', // Optional, defaults to '/api'
|
|
285
|
+
});
|
|
210
286
|
|
|
211
|
-
|
|
287
|
+
// Add your books resource and helpers here (same as before)
|
|
288
|
+
// ...
|
|
212
289
|
|
|
213
|
-
|
|
290
|
+
// Create Express app
|
|
291
|
+
const app = express();
|
|
214
292
|
|
|
215
|
-
|
|
293
|
+
// Mount the API routes - both approaches work:
|
|
216
294
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
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
|
-
//
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
//
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
347
|
+
// Start the HTTP server - multiple options:
|
|
278
348
|
|
|
279
|
-
|
|
349
|
+
// Option 1: Start on configured port (3000 by default)
|
|
350
|
+
api.http.startServer();
|
|
280
351
|
|
|
281
|
-
|
|
282
|
-
api.
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
295
|
-
const history = await api.getVersionHistory('users', user.id);
|
|
378
|
+
#### Character Encoding
|
|
296
379
|
|
|
297
|
-
|
|
298
|
-
await api.restoreVersion('users', user.id, 1);
|
|
380
|
+
The plugin respects the charset parameter in Content-Type headers:
|
|
299
381
|
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
388
|
+
The HTTP plugin provides the same REST endpoints as Express but with zero external framework dependencies.
|
|
305
389
|
|
|
306
|
-
|
|
390
|
+
### Using Your REST API
|
|
307
391
|
|
|
308
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
325
|
-
api.
|
|
326
|
-
|
|
511
|
+
// Use the Knex plugin
|
|
512
|
+
api.use(RestApiPlugin);
|
|
513
|
+
api.use(RestApiKnexPlugin, {
|
|
514
|
+
knex: { knex: db }
|
|
327
515
|
});
|
|
328
516
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
564
|
+
#### File Parsers
|
|
367
565
|
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
705
|
+
#### LocalStorage
|
|
706
|
+
|
|
707
|
+
The LocalStorage adapter saves files to your local filesystem:
|
|
391
708
|
|
|
392
709
|
```javascript
|
|
393
|
-
import {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
741
|
+
#### S3Storage (Mock Implementation)
|
|
408
742
|
|
|
409
|
-
|
|
743
|
+
The package includes a mock S3 storage adapter for development:
|
|
410
744
|
|
|
411
745
|
```javascript
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
+
For now, you have everything you need to create a basic REST API with file upload support!
|