just-another-http-api 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/api.js +166 -0
  4. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Oliver Edgington
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ ![just-another-http-api](https://i.ibb.co/rZh9CkG/logo.png "Just Another HTTP API")
2
+
3
+ [![Run Unit Tests](https://github.com/OllieEdge/just-another-http-api/actions/workflows/main.yml/badge.svg)](https://github.com/OllieEdge/just-another-http-api/actions/workflows/main.yml)
4
+ [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/)
5
+ [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE)
6
+ [![Awesome Badges](https://img.shields.io/badge/badges-awesome-green.svg)](https://github.com/Naereen/badges)
7
+
8
+
9
+ # Just Another HTTP API
10
+ A framework built on top of restify aimed at removing the need for any network or server configuration. You can install this package and immediately begin coding logic for your endpoints.
11
+
12
+ This framework scans your `./routes` directory (*configurable*) and automatically builds all of your endpoints using the file and folder structure you setup. For each endpoint a single `*.js` will be able to split logic based on the method of the requests.
13
+
14
+
15
+ ## Quickstart
16
+ - Install package: `npm i just-another-http-api`
17
+ - Make routes directory: `mkdir routes`
18
+ - Add the following to your app.js:
19
+ ```
20
+ const API = require('just-another-http-api');
21
+
22
+ const server = await API();
23
+ // server is now ready.
24
+ ```
25
+ - Add a route: `touch ./routes/endpoint.js`
26
+ - Handle request in your new endpoint:
27
+ ```
28
+ exports.get = async req => {
29
+ return {
30
+ "hello": "world"
31
+ };
32
+ }
33
+ ```
34
+ - Run server: `npm start`
35
+ - Load http://localhost:4001/endpoint in your browser and you will see the JSON response.
36
+
37
+
38
+ ## Supported Node Versions
39
+ This is a personal project I've modified a few times over the years, I've just cleaned everything up and made OpenSource, in the process I've made it compatible with the latest Node as of May 2022 (v18), previous node versions are not supported.
40
+
41
+ | Node Release | Supported in Current Version | Notes |
42
+ | :--: | :---: | :---: |
43
+ | 18.x | **Yes** | Current stable |
44
+ | <18 | **No** | No Support |
45
+
46
+ # Usage
47
+ ## Initialise server
48
+ Pretty simple to get the server running, use one of the following options:
49
+ ```
50
+ const API = require ( 'just-another-http-api' );
51
+
52
+ // Option 1
53
+ const server = await API();
54
+
55
+ // Option 2 - you should use this option.Find more about config settings below.
56
+ const config = {
57
+ docRoot: 'customDir',
58
+ bodyParser: true,
59
+ queryParser: true
60
+ }
61
+ const server = await API( config );
62
+
63
+ // Option 3
64
+ const restify = require ( 'restify' );
65
+ const config = {
66
+ docRoot: 'customDir',
67
+ bodyParser: true,
68
+ queryParser: true
69
+ }
70
+ const reportAnalytics = ( endpointUsage => { console.log ( endpointUsage.path ) } );
71
+ const customServer = restify.createServer();
72
+ const server = await API( config, reportAnalytics, customServer );
73
+ ```
74
+
75
+ If you need to shut the server down, make sure you have stored the server instance and call its `close()` method. eg:
76
+ ```
77
+ server.close();
78
+ ```
79
+
80
+ ## Set up your endpoints
81
+ Create routes using your file and folder structure. By default Just Another HTTP API will look for a routes folder in your working directory, you can change this by parsing a docRoot in the config.
82
+ ```
83
+ // Config Object
84
+
85
+ {
86
+ docRoot: 'customFolder'
87
+ }
88
+ ```
89
+
90
+ If your folder structure was the following:
91
+ ```
92
+ root/
93
+ ├── app.js
94
+ ├── routes/
95
+ │ ├── index.js
96
+ │ ├── users/
97
+ │ │ ├── index.js
98
+ │ │ ├── userId.js
99
+ │ └── atoms/
100
+ │ ├── index.js
101
+ │ ├── atomId.js
102
+ │ ├── protons/
103
+ │ │ └── protonId.js
104
+ │ ├── neutrons/
105
+ │ │ └── neutronId.js
106
+ │ └── electrons/
107
+ │ └── electronId.js
108
+ └── package.json
109
+ ```
110
+ It would give you the following endpoints:
111
+ ```
112
+ Endpoint File used
113
+ -------- ---------
114
+ ... (routes/index.js)
115
+ .../users (routes/users/index.js)
116
+ .../users/:userId (routes/users/userId.js)
117
+ .../atoms (routes/atoms/index.js)
118
+ .../atoms/:atomId (routes/atoms/atomId.js)
119
+ .../atoms/protons/:protonId (routes/atoms/protons/protonId.js)
120
+ .../atoms/neutrons/:neutronId (routes/atoms/neutrons/neutronId.js)
121
+ .../atoms/electrons/:electronsId (routes/atoms/electrons/electronsId.js)
122
+ ```
123
+ ---
124
+ ### Configuring your endpoint logic
125
+ Each endpoint can export any of the follow http methods: `head, get, post, put, patch, del`
126
+
127
+ Just Another HTTP API doesn't really care about the method you are using, there is not logical difference so it will be up to you to decide how you implement them. Typically HEAD and GET never changes anything it only returns information, whereas PUT, POST, PATCH and DEL will most likely modify some kind of data. More about HTTP Methods can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
128
+
129
+ For this example we are going to be using a file located at `./routes/users/userId.js` in our project. It will accept GET and POST methods. Here is the code for this file:
130
+ ```
131
+ exports.get = async req => {
132
+
133
+ // Because this filename has 'Id' in its title it will extract the userId provided in the request and make it avaiable via the request parameters
134
+ const { userId } = req.params;
135
+
136
+ // Connect to a datastore and retrieve user data using the userId provided in the params.
137
+ const user = await db.query(`SELECT * FROM users WHERE id = ${ userId }`);
138
+
139
+ // Return the data in the response
140
+ return user;
141
+ }
142
+
143
+ exports.post = async req => {
144
+
145
+ // extract the JSON we sent in the request. Requires config.bodyParser to be true
146
+ const { body } = req;
147
+ const { userId } = req.params;
148
+
149
+ // Update a user login goes here
150
+ const user = {};
151
+
152
+ return user;
153
+ }
154
+
155
+ exports.put = async req => { throw new Error ( 'Not configured' ) };
156
+ exports.del = async req => { throw new Error ( 'Not configured' ) };
157
+ exports.patch = async req => { throw new Error ( 'Not configured' ) };
158
+ ```
159
+ ---
160
+ ## Example configuration
161
+ When initialising the API you can parse it a config object. Below is a typical variant:
162
+ ```
163
+ {
164
+ bodyParser: true,
165
+ queryParser: true,
166
+ uploads: {
167
+ enabled: true
168
+ },
169
+ docRoot: './routes',
170
+ port: 4001,
171
+ cors: {
172
+ credentials: false,
173
+ origins: [ '*' ],
174
+ allowHeaders: [
175
+ 'accept',
176
+ 'accept-version',
177
+ 'content-type',
178
+ 'request-id',
179
+ 'origin',
180
+ 'x-api-version',
181
+ 'x-request-id'
182
+ ],
183
+ exposeHeaders: [
184
+ 'accept',
185
+ 'accept-version',
186
+ 'content-type',
187
+ 'request-id',
188
+ 'origin',
189
+ 'x-api-version',
190
+ 'x-request-id'
191
+ ],
192
+ }
193
+ };
194
+ ```
195
+
196
+ | Config option | Default | Type | Description | Notes |
197
+ | :--: | :--: | :--: | :---: | :---: |
198
+ | bodyParser | `false` | Boolean | Will enable the `.body` attribute of the `request` object | -
199
+ | queryParser | `false` | Boolean | Will enable the `.query` attribute of the `request` object | Query parameters will be stored in a key/value object
200
+ | uploads | `null` | Object | Enables the `application/octet-stream` content-type | Supported by the multer module, see more here for config options: https://github.com/expressjs/multer
201
+ | docRoot | `./routes` | String | Changes the directory to map endpoints with | -
202
+ | port | `4001` | Integer | Set the port number the server runs on | -
203
+ | cors | `null` | Object | Set the default cors | -
204
+
205
+
206
+ ## LICENSE
207
+ MIT License
208
+
209
+ Copyright (c) 2022 Oliver Edgington
210
+
211
+ Permission is hereby granted, free of charge, to any person obtaining a copy
212
+ of this software and associated documentation files (the "Software"), to deal
213
+ in the Software without restriction, including without limitation the rights
214
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
215
+ copies of the Software, and to permit persons to whom the Software is
216
+ furnished to do so, subject to the following conditions:
217
+
218
+ The above copyright notice and this permission notice shall be included in all
219
+ copies or substantial portions of the Software.
220
+
221
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
222
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
223
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
224
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
225
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
226
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
227
+ SOFTWARE.
228
+
229
+
230
+ ## Contact & Links
231
+ Oliver Edgington <oliver@edgington.com>
232
+
233
+ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40OllieEdge)](https://twitter.com/OllieEdge)
package/api.js ADDED
@@ -0,0 +1,166 @@
1
+ const restify = require ( 'restify' );
2
+ const restifyErrors = require ( 'restify-errors' );
3
+ const corsPlugin = require ( 'restify-cors-middleware2' );
4
+ const recursiveRead = require ( 'recursive-readdir' );
5
+ const packageJson = require ( './package.json' );
6
+ const path = require ( 'path' );
7
+ const multer = require ( 'multer' );
8
+ const storage = multer.memoryStorage ();
9
+
10
+ /**
11
+ * See README for config setup.
12
+ *
13
+ * @param {Object} config The config, see readme for example configurations
14
+ * @param {Function} every An optional agrument that accepts a function ready to receive an object. The function will be called everytime a endpoint is requested (good for analytical usage)
15
+ * @param {RestifyServer} _server Optionally override the restify instance in this API and use your own. Accepts a `restify.createServer()` instance.
16
+ */
17
+ module.exports = async ( config, every = null, _server = null ) => {
18
+
19
+ if ( !config ) console.log ( 'JustAnother: WARNING: You\'ve initialised Just Another Http API without any config. This is not recommended.' );
20
+
21
+ let upload;
22
+ let server = _server;
23
+
24
+ if ( _server ) console.debug ( 'JustAnother: Using restify override instance provided.' );
25
+ else {
26
+ server = restify.createServer ( {
27
+ name: packageJson.name,
28
+ version: packageJson.version
29
+ } );
30
+
31
+ if ( config?.bodyParser ) server.use ( restify.plugins.queryParser () );
32
+ if ( config?.bodyParser ) server.use ( restify.plugins.bodyParser () );
33
+ if ( config?.uploads && config?.uploads.enabled ) upload = multer ( { storage: storage } );
34
+
35
+ if ( config?.cors ){
36
+ const cors = corsPlugin ( config.cors );
37
+ server.pre ( cors.preflight );
38
+ server.use ( cors.actual );
39
+ }
40
+ }
41
+
42
+ server.on ( 'MethodNotAllowed', unknownMethodHandler );
43
+
44
+ const files = await recursiveReadDir ( config?.docRoot || 'routes' );
45
+ const endpoints = files.map ( ( filePath ) => ( {
46
+ handlers: require ( path.resolve ( filePath ) ),
47
+ path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
48
+ } ) );
49
+
50
+ endpoints.forEach ( endpoint => {
51
+ Object.keys ( endpoint.handlers ).forEach ( method => {
52
+ const endpointArgs = [
53
+ endpoint.path,
54
+ method === 'post' && upload ? upload.single ( 'file' ) : null
55
+ ].filter ( Boolean );
56
+
57
+ server[ method ] ( ...endpointArgs, async ( req, res ) => {
58
+ try {
59
+ const response = await endpoint.handlers[ method ] ( req );
60
+
61
+ if ( every ) {
62
+ every ( { path: endpoint.path, method, req } );
63
+ }
64
+
65
+ // If optional headers have been provided in the response add them here.
66
+ if ( response.hasOwnProperty ( 'headers' ) ){
67
+ res.set ( response.headers );
68
+ }
69
+
70
+ // If response.html is set, we want to send the HTML back as a raw string and set the content type.
71
+ if ( response.hasOwnProperty ( 'html' ) ){
72
+ res.sendRaw ( 200, response.html, { 'Content-Type': 'text/html' } );
73
+ } //
74
+ else if ( response.hasOwnProperty ( 'json' ) || response.hasOwnProperty ( 'body' ) || response.hasOwnProperty ( 'response' ) || typeof response === 'string' ){
75
+ data = response?.json || response?.body || response?.response || response;
76
+ res.send ( method === 'post' ? 201 : 200, data );
77
+ }
78
+ else if ( response.hasOwnProperty ( 'error' ) ){
79
+ res.send ( new restifyErrors.makeErrFromCode ( response?.error?.statusCode, response?.error?.message ) );
80
+ }
81
+ else if ( response.hasOwnProperty ( 'file' ) ){
82
+ res.sendRaw ( response.file );
83
+ }
84
+ else if ( typeof response === 'object' ){
85
+ res.send ( response ); //Try and send whatever it is
86
+ }
87
+ else if ( !response ){
88
+ res.send ( 204 );
89
+ }
90
+ else {
91
+ res.send ( new restifyErrors.makeErrFromCode ( 500, `Just Another Http API did not understand the response provided for request: ${ method } to ${ endpoint.path }. Check your return value.` ) );
92
+ }
93
+
94
+ return;
95
+ }
96
+ catch ( error ){
97
+ if ( error instanceof Error ) {
98
+ res.send ( new restifyErrors.InternalServerError ( { code: 500 }, error.stack.replace ( /\n/g, ' ' ) ) );
99
+ }
100
+ else {
101
+ if ( error.code ) {
102
+ res.send ( new restifyErrors.makeErrFromCode ( error.code, error.message ) );
103
+ }
104
+
105
+ res.send ( new restifyErrors.InternalServerError ( { code: 500 }, JSON.stringify ( error, null, 2 ) ) );
106
+ }
107
+
108
+ return;
109
+ }
110
+ } );
111
+ } );
112
+ } );
113
+
114
+ await server.listen ( process.env.PORT || config?.port || 4001 );
115
+
116
+ return server;
117
+ };
118
+
119
+ const handlerPathToApiPath = ( path, docRoot ) => {
120
+
121
+ const targetPath = path.replace ( docRoot.replace ( /.\//igm, '' ), '' ).split ( '/' );
122
+
123
+ return '/' + targetPath.map ( subPath => {
124
+ if ( subPath.includes ( 'index.js' ) && path.substr ( path.lastIndexOf ( '/' ) ).includes ( 'index' ) ) return null;
125
+ if ( ( subPath.includes ( '-' ) || subPath.includes ( 'Id' ) ) && ( subPath.includes ( '.js' ) || subPath.includes ( 'Id' ) ) ) {
126
+ subPath = subPath.split ( '-' ).map ( urlParameter => {
127
+ urlParameter = urlParameter.replace ( /-/igm, '' );
128
+
129
+ return ':' + urlParameter;
130
+ } ).join ( '/' );
131
+ }
132
+
133
+ return subPath.replace ( /.js/igm, '' );
134
+ } ).filter ( Boolean ).join ( '/' );
135
+
136
+ };
137
+
138
+ const recursiveReadDir = async ( docRoot ) => {
139
+ try {
140
+ const files = await recursiveRead ( docRoot );
141
+
142
+ // Remove all falsy values and reverse the array.
143
+ return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
144
+ }
145
+ catch ( e ){
146
+ console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
147
+ throw e;
148
+ }
149
+ };
150
+
151
+ const unknownMethodHandler = ( req, res ) => {
152
+ if ( req.method.toLowerCase () === 'options' ) {
153
+ const allowHeaders = [ '*' ];
154
+
155
+ if ( res.methods.indexOf ( 'OPTIONS' ) === -1 ) res.methods.push ( 'OPTIONS' );
156
+
157
+ res.header ( 'Access-Control-Allow-Credentials', true );
158
+ res.header ( 'Access-Control-Allow-Headers', allowHeaders.join ( ', ' ) );
159
+ res.header ( 'Access-Control-Allow-Methods', res.methods.join ( ', ' ) );
160
+ res.header ( 'Access-Control-Allow-Origin', req.headers.origin );
161
+
162
+ return res.send ( 204 );
163
+ }
164
+
165
+ return res.send ( new restifyErrors.MethodNotAllowedError ( { code: 405 }, `${ req.method } method is not available on this endpoint` ) );
166
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "just-another-http-api",
3
+ "version": "1.0.1",
4
+ "description": "A framework built on top of restify aimed at removing the need for any network or server configuration. ",
5
+ "homepage": "https://github.com/OllieEdge/just-another-http-api#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/OllieEdge/just-another-http-api.git"
9
+ },
10
+ "main": "api.js",
11
+ "scripts": {
12
+ "start": "node api.js",
13
+ "test": "mocha"
14
+ },
15
+ "keywords": [
16
+ "restapi",
17
+ "restful",
18
+ "rest",
19
+ "restify",
20
+ "http",
21
+ "server",
22
+ "just another",
23
+ "another"
24
+ ],
25
+ "author": "Oliver Edgington <oliver@edgington.com> (https://github.com/OllieEdge)",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "multer": "^1.4.2",
29
+ "recursive-readdir": "^2.2.2",
30
+ "restify": "github:restify/node-restify#c5361e9",
31
+ "restify-cors-middleware2": "^2.1.2",
32
+ "restify-errors": "^8.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "chai": "^4.3.6",
36
+ "chai-as-promised": "^7.1.1",
37
+ "mocha": "^10.0.0"
38
+ }
39
+ }