@stanlemon/server 0.2.61 → 0.3.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/README.md +2 -0
- package/app.js +2 -0
- package/package.json +6 -4
- package/src/asyncJsonHandler.js +16 -19
- package/src/createAppServer.js +75 -21
- package/src/schemaHandler.js +15 -7
- package/test.http +22 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Express App Server
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/js/%40stanlemon%2Fserver)
|
|
4
|
+
|
|
3
5
|
This is a base express app server that is wired up with sensible defaults, like compression, json support and serving the `dist` folder statically.
|
|
4
6
|
|
|
5
7
|
It also include a function called `asyncJsonHandler` which is a wrapper for most express requests. It standardizes input/output as JSON, accepting camel case properties and snake casing them for output. It also covers a bunch of standard error behaviors.
|
package/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stanlemon/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A basic express web server setup.",
|
|
5
5
|
"author": "Stan Lemon <stanlemon@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,20 +13,22 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "NODE_ENV=development nodemon ./app.js",
|
|
15
15
|
"lint": "eslint --ext js,jsx,ts,tsx ./",
|
|
16
|
-
"lint:
|
|
16
|
+
"lint:fix": "eslint --fix --ext js,jsx,ts,tsx ./",
|
|
17
17
|
"test": "jest --detectOpenHandles",
|
|
18
18
|
"test:coverage": "jest --detectOpenHandles --coverage",
|
|
19
19
|
"test:watch": "jest --detectOpenHandles --watch"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@stanlemon/webdev": "*",
|
|
23
|
+
"body-parser": "^1.20.2",
|
|
23
24
|
"compression": "^1.7.4",
|
|
25
|
+
"cookie-parser": "^1.4.6",
|
|
24
26
|
"dotenv": "16.3.1",
|
|
25
27
|
"express": "^4.18.2",
|
|
26
28
|
"express-rate-limit": "^7.1.5",
|
|
27
29
|
"helmet": "^7.1.0",
|
|
28
30
|
"http-proxy-middleware": "^2.0.6",
|
|
29
|
-
"joi": "^17.
|
|
31
|
+
"joi": "^17.12.0",
|
|
30
32
|
"lodash": "^4.17.21",
|
|
31
33
|
"lodash-es": "^4.17.21",
|
|
32
34
|
"morgan": "^1.10.0"
|
|
@@ -34,6 +36,6 @@
|
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@stanlemon/eslint-config": "*",
|
|
36
38
|
"@types/lodash": "^4.14.202",
|
|
37
|
-
"nodemon": "^3.0.
|
|
39
|
+
"nodemon": "^3.0.3"
|
|
38
40
|
}
|
|
39
41
|
}
|
package/src/asyncJsonHandler.js
CHANGED
|
@@ -4,12 +4,20 @@ import { formatOutput } from "./index.js";
|
|
|
4
4
|
|
|
5
5
|
export default asyncJsonHandler;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Handler for JSON responses.
|
|
9
|
+
* This method ensures data formatting using {formatInput} and {formatOutput}.
|
|
10
|
+
* Callbacks that are wrapped by this handler receive an additional argument {input} which contains the formatted input data.
|
|
11
|
+
* If the request is a GET, the {input} is the request.params object.
|
|
12
|
+
* If the request is a POST or PUT, the {input} is the request.body object.
|
|
13
|
+
* @param {function} Express route handler function
|
|
14
|
+
*/
|
|
7
15
|
export function asyncJsonHandler(fn) {
|
|
8
|
-
return async (req, res
|
|
9
|
-
const input = buildInput(req);
|
|
10
|
-
|
|
16
|
+
return async (req, res, next) => {
|
|
11
17
|
try {
|
|
12
|
-
const
|
|
18
|
+
const input = buildInput(req);
|
|
19
|
+
|
|
20
|
+
const output = await fn(input, req, res, next);
|
|
13
21
|
|
|
14
22
|
// If a value is returned we'll assume that we need to render it as JSON
|
|
15
23
|
if (output !== undefined) {
|
|
@@ -50,23 +58,12 @@ export function asyncJsonHandler(fn) {
|
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
function buildInput(req) {
|
|
53
|
-
const query = isPlainObject(req.query) ? req.query : {};
|
|
54
|
-
const params = isPlainObject(req.params) ? req.params : {};
|
|
55
|
-
|
|
56
61
|
if (req.method === "POST" || req.method === "PUT") {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return body;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Always make sure the request parameters override everything else
|
|
65
|
-
// eg. a param for 'id' is not overridden by a query string 'id' or 'req.body.id'
|
|
66
|
-
return { ...body, ...query, ...params };
|
|
62
|
+
return formatInput(req.body);
|
|
63
|
+
} else if (req.method === "GET" || req.method === "DELETE") {
|
|
64
|
+
const params = isPlainObject(req.params) ? req.params : {};
|
|
65
|
+
return formatInput(params);
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
return params;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
function formatError(ex) {
|
package/src/createAppServer.js
CHANGED
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
import dotenv from "dotenv";
|
|
2
|
-
import express from "express";
|
|
2
|
+
import express, { Router } from "express";
|
|
3
|
+
import cookieParser from "cookie-parser";
|
|
3
4
|
import compression from "compression";
|
|
4
5
|
import helmet from "helmet";
|
|
5
6
|
import morgan from "morgan";
|
|
6
7
|
import rateLimit from "express-rate-limit";
|
|
7
8
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
9
|
+
import path from "path";
|
|
8
10
|
|
|
9
11
|
dotenv.config();
|
|
10
12
|
|
|
13
|
+
const NODE_ENV = process.env.NODE_ENV ?? "development";
|
|
14
|
+
|
|
11
15
|
export const DEFAULTS = { port: 3000, webpack: false, start: true };
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} AppAddons
|
|
19
|
+
* @property {function} start Start the server
|
|
20
|
+
* @property {function} catch404s Enable the 404 handler, only call this after you have added all of your routers.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {import("express").Express & AppAddons} AppServer
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an express application server.
|
|
27
|
+
* @param {object} options
|
|
28
|
+
* @param {number} options.port Port to listen on
|
|
29
|
+
* @param {boolean} options.webpack Whether or not to create a proxy for webpack
|
|
30
|
+
* @param {boolean} options.start Whether or not to start the server
|
|
31
|
+
* @returns {AppServer} Pre-configured express app server with extra helper methods
|
|
32
|
+
*/
|
|
33
|
+
// eslint-disable-next-line max-lines-per-function
|
|
13
34
|
export default function createAppServer(options) {
|
|
14
35
|
const { port, webpack, start } = { ...DEFAULTS, ...options };
|
|
15
36
|
|
|
37
|
+
const useWebpack =
|
|
38
|
+
webpack !== false && NODE_ENV !== "production" && NODE_ENV !== "test";
|
|
39
|
+
|
|
16
40
|
const app = express();
|
|
17
|
-
app.use(express.
|
|
41
|
+
app.use(express.urlencoded({ extended: true }));
|
|
42
|
+
app.use(express.json());
|
|
43
|
+
app.use(cookieParser());
|
|
18
44
|
|
|
19
|
-
if (
|
|
45
|
+
if (NODE_ENV !== "test") {
|
|
20
46
|
app.use(morgan("combined"));
|
|
21
47
|
}
|
|
22
48
|
|
|
23
|
-
if (
|
|
49
|
+
if (NODE_ENV === "production") {
|
|
24
50
|
const limiter = rateLimit({
|
|
25
51
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
26
52
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
@@ -33,13 +59,18 @@ export default function createAppServer(options) {
|
|
|
33
59
|
app.use(helmet());
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
62
|
+
app.get("/ping", (req, res) => {
|
|
63
|
+
res.json({
|
|
64
|
+
success: true,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (useWebpack) {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.info("Proxying webpack dev server");
|
|
71
|
+
|
|
41
72
|
app.get(
|
|
42
|
-
"
|
|
73
|
+
"/static/*",
|
|
43
74
|
createProxyMiddleware({
|
|
44
75
|
target: webpack,
|
|
45
76
|
changeOrigin: true,
|
|
@@ -66,25 +97,48 @@ export default function createAppServer(options) {
|
|
|
66
97
|
app.use(express.static("./dist"));
|
|
67
98
|
}
|
|
68
99
|
|
|
69
|
-
app.
|
|
70
|
-
res.json({
|
|
71
|
-
success: true,
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// If we're set to start. Btw we never start in test.
|
|
76
|
-
if (start && process.env.NODE_ENV !== "test") {
|
|
100
|
+
app.start = () => {
|
|
77
101
|
const server = app.listen(port);
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
console.
|
|
81
|
-
console
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.info("Starting in %s mode on port %s", NODE_ENV, port);
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.info(
|
|
82
107
|
"Listening at http://%s:%s",
|
|
83
108
|
server.address().address === "::"
|
|
84
109
|
? "localhost"
|
|
85
110
|
: server.address().address,
|
|
86
111
|
server.address().port
|
|
87
112
|
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
app.spa = () => {
|
|
116
|
+
if (useWebpack) {
|
|
117
|
+
app.get(
|
|
118
|
+
"*",
|
|
119
|
+
createProxyMiddleware({
|
|
120
|
+
target: webpack,
|
|
121
|
+
changeOrigin: true,
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
app.get(`*`, function (req, res, next) {
|
|
126
|
+
res.sendFile(path.resolve("./", "dist", "index.html"));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
app.catch404s = (path = "/*") => {
|
|
132
|
+
const router = Router();
|
|
133
|
+
router.use((req, res, next) => {
|
|
134
|
+
res.status(404).json({ error: "Not Found" });
|
|
135
|
+
});
|
|
136
|
+
app.use(path, router);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// If we're set to start. Btw we never start in test.
|
|
140
|
+
if (start && NODE_ENV !== "test") {
|
|
141
|
+
app.start();
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
return app;
|
package/src/schemaHandler.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
1
2
|
import asyncJsonHandler from "./asyncJsonHandler.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param {Joi.Schema} schema
|
|
7
|
+
* @param {*} fn
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
3
10
|
export default function schemaHandler(schema, fn) {
|
|
4
11
|
return async (req, res, next) => {
|
|
5
12
|
// Validate the input schema
|
|
@@ -27,18 +34,19 @@ export default function schemaHandler(schema, fn) {
|
|
|
27
34
|
},
|
|
28
35
|
});
|
|
29
36
|
|
|
30
|
-
// eslint-disable-next-line require-atomic-updates
|
|
31
37
|
req.body = value;
|
|
32
38
|
|
|
33
39
|
// Wrap all of these in our async handler
|
|
34
40
|
await asyncJsonHandler(fn)(req, res, next);
|
|
35
41
|
} catch (error) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
if (error instanceof Joi.ValidationError) {
|
|
43
|
+
res.status(400).json({
|
|
44
|
+
errors: Object.assign.apply(
|
|
45
|
+
null,
|
|
46
|
+
error.details.map((d) => ({ [d.path]: d.message }))
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
};
|
|
44
52
|
}
|
package/test.http
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
GET http://localhost:3003/hello
|
|
2
|
+
|
|
3
|
+
###
|
|
4
|
+
|
|
5
|
+
GET http://localhost:3003/hello/Stan
|
|
6
|
+
|
|
7
|
+
###
|
|
8
|
+
|
|
9
|
+
GET http://localhost:3003/hello/Sara
|
|
10
|
+
|
|
11
|
+
###
|
|
12
|
+
|
|
13
|
+
POST http://localhost:3003/hello HTTP/1.1
|
|
14
|
+
Content-Type: application/json
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
"name": "Lucy"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
###
|
|
21
|
+
|
|
22
|
+
GET http://localhost:3003/doesNotExist
|