@stanlemon/server 0.1.0
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/.eslintrc.json +5 -0
- package/README.md +49 -0
- package/app.js +37 -0
- package/package.json +32 -0
- package/src/asyncJsonHandler.js +105 -0
- package/src/convertCase.js +20 -0
- package/src/createAppServer.js +74 -0
- package/src/formatInput.js +6 -0
- package/src/formatOutput.js +12 -0
- package/src/index.js +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Express App Server
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
When `NODE_ENV=development` the server will also proxy requests to webpack.
|
|
8
|
+
|
|
9
|
+
This library goes well with [@stanlemon/webdev](../webdev/README.md).
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
import {
|
|
13
|
+
createAppServer,
|
|
14
|
+
asyncJsonHandler as handler,
|
|
15
|
+
NotFoundException,
|
|
16
|
+
} from "@stanlemon/server";
|
|
17
|
+
|
|
18
|
+
const app = createAppServer({ port: 3003 });
|
|
19
|
+
|
|
20
|
+
// curl http://localhost:3003/hello?name=Stanley
|
|
21
|
+
app.get(
|
|
22
|
+
"/hello",
|
|
23
|
+
handler(({ name }) => ({ hello: name || "Stan" }))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// curl http://localhost:3003/hello/Stan
|
|
27
|
+
app.get(
|
|
28
|
+
"/hello/Stan",
|
|
29
|
+
// Promises are handled correctly
|
|
30
|
+
handler(() => {
|
|
31
|
+
// This is the same as just hitting /hello, so we'll demonstrate the exception handling
|
|
32
|
+
throw new NotFoundException();
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// curl http://localhost:3003/hello/Stanley
|
|
37
|
+
app.get(
|
|
38
|
+
"/hello/:name",
|
|
39
|
+
// Promises are handled correctly
|
|
40
|
+
handler(({ name }) => Promise.resolve({ hello: name || "Stan" }))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// curl -X POST http://localhost:3003/hello -H 'Content-Type: application/json' -d '{"name": "Stanley"}'
|
|
44
|
+
app.post(
|
|
45
|
+
"/hello",
|
|
46
|
+
// You can also use async/await
|
|
47
|
+
handler(async ({ name }) => await Promise.resolve({ hello: name || "Stan" }))
|
|
48
|
+
);
|
|
49
|
+
```
|
package/app.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppServer,
|
|
3
|
+
asyncJsonHandler as handler,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
} from "./src/index.js";
|
|
6
|
+
|
|
7
|
+
const app = createAppServer({ port: 3003 });
|
|
8
|
+
|
|
9
|
+
// curl http://localhost:3003/hello?name=Stanley
|
|
10
|
+
app.get(
|
|
11
|
+
"/hello",
|
|
12
|
+
handler(({ name }) => ({ hello: name || "Stan" }))
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// curl http://localhost:3003/hello/Stan
|
|
16
|
+
app.get(
|
|
17
|
+
"/hello/Stan",
|
|
18
|
+
// Promises are handled correctly
|
|
19
|
+
handler(() => {
|
|
20
|
+
// This is the same as just hitting /hello, so we'll demonstrate the exception handling
|
|
21
|
+
throw new NotFoundException();
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// curl http://localhost:3003/hello/Stanley
|
|
26
|
+
app.get(
|
|
27
|
+
"/hello/:name",
|
|
28
|
+
// Promises are handled correctly
|
|
29
|
+
handler(({ name }) => Promise.resolve({ hello: name || "Stan" }))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// curl -X POST http://localhost:3003/hello -H 'Content-Type: application/json' -d '{"name": "Stanley"}'
|
|
33
|
+
app.post(
|
|
34
|
+
"/hello",
|
|
35
|
+
// You can also use async/await
|
|
36
|
+
handler(async ({ name }) => await Promise.resolve({ hello: name || "Stan" }))
|
|
37
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stanlemon/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A basic express web server setup.",
|
|
5
|
+
"author": "Stan Lemon <stanlemon@users.noreply.github.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=17.0"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./src/index.js",
|
|
12
|
+
"exports": "./src/index.js",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "NODE_ENV=development nodemon ./app.js",
|
|
15
|
+
"lint": "eslint --ext js,jsx,ts,tsx ./",
|
|
16
|
+
"lint:format": "eslint --fix --ext js,jsx,ts,tsx ./"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"compression": "^1.7.4",
|
|
20
|
+
"dotenv": "11.0.0",
|
|
21
|
+
"express": "^4.17.1",
|
|
22
|
+
"express-rate-limit": "^5.5.1",
|
|
23
|
+
"helmet": "^4.6.0",
|
|
24
|
+
"http-proxy-middleware": "^2.0.1",
|
|
25
|
+
"lodash-es": "^4.17.21",
|
|
26
|
+
"morgan": "^1.10.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@stanlemon/eslint-config": "*",
|
|
30
|
+
"nodemon": "^2.0.15"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { formatInput } from "./index.js";
|
|
2
|
+
import { formatOutput } from "./index.js";
|
|
3
|
+
|
|
4
|
+
export default asyncJsonHandler;
|
|
5
|
+
|
|
6
|
+
export function asyncJsonHandler(fn) {
|
|
7
|
+
return async (req, res /*, next */) => {
|
|
8
|
+
const input = {
|
|
9
|
+
// For POST & PUT requests we'll use the body
|
|
10
|
+
// For everything else, it's query string parameters
|
|
11
|
+
...(req.method === "POST" || req.method === "PUT"
|
|
12
|
+
? formatInput(req.body)
|
|
13
|
+
: req.query || {}),
|
|
14
|
+
// Always make sure the request parameters override everything else
|
|
15
|
+
// eg. a param for 'id' is not overridden by a query string 'id' or 'req.body.id'
|
|
16
|
+
...req.params,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const output = await fn(input);
|
|
21
|
+
|
|
22
|
+
// If a value is returned we'll assume that we need to render it as JSON
|
|
23
|
+
if (output !== undefined) {
|
|
24
|
+
res.status(200).json(formatOutput(output));
|
|
25
|
+
}
|
|
26
|
+
} catch (ex) {
|
|
27
|
+
// TODO: Add better support for validation errors
|
|
28
|
+
if (ex.message === "Bad Request") {
|
|
29
|
+
res.status(400).json({ error: ex.message });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ex.message === "Not Authorized") {
|
|
34
|
+
res.status(403).json({ error: ex.message });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ex.message === "Not Found") {
|
|
39
|
+
res.status(404).json({ error: ex.message });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (ex.message === "Already Exists") {
|
|
44
|
+
res.status(409).json({ error: ex.message });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
process.env.NODE_ENV === "development" ||
|
|
50
|
+
process.env.NODE_ENV === "test"
|
|
51
|
+
) {
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.error(ex);
|
|
54
|
+
|
|
55
|
+
res.status(500).json({ error: ex.message });
|
|
56
|
+
} else {
|
|
57
|
+
res.status(500).json({ error: "Something went wrong" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class BadRequestException extends Error {
|
|
64
|
+
static MESSAGE = "Bad Request";
|
|
65
|
+
static CODE = 400;
|
|
66
|
+
|
|
67
|
+
constructor() {
|
|
68
|
+
super(BadRequestException.MESSAGE);
|
|
69
|
+
this.name = "BadRequestException";
|
|
70
|
+
this.code = BadRequestException.CODE;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class NotAuthorizedException extends Error {
|
|
75
|
+
static MESSAGE = "Not Authorized";
|
|
76
|
+
static CODE = 403;
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super(NotAuthorizedException.MESSAGE);
|
|
80
|
+
this.name = "NotAuthorizedException";
|
|
81
|
+
this.code = NotAuthorizedException.CODE;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class NotFoundException extends Error {
|
|
86
|
+
static MESSAGE = "Not Found";
|
|
87
|
+
static CODE = 404;
|
|
88
|
+
|
|
89
|
+
constructor() {
|
|
90
|
+
super(NotFoundException.MESSAGE);
|
|
91
|
+
this.name = "NotFoundException";
|
|
92
|
+
this.code = NotFoundException.CODE;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class AlreadyExistsException extends Error {
|
|
97
|
+
static MESSAGE = "Already Exists";
|
|
98
|
+
static CODE = 409;
|
|
99
|
+
|
|
100
|
+
constructor() {
|
|
101
|
+
super(AlreadyExistsException.MESSAGE);
|
|
102
|
+
this.name = "AlreadyExistsException";
|
|
103
|
+
this.code = AlreadyExistsException.CODE;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isObject, isArray, isString } from "lodash-es";
|
|
2
|
+
|
|
3
|
+
export default function convertCase(obj, me, convert) {
|
|
4
|
+
if (
|
|
5
|
+
isString(obj) &&
|
|
6
|
+
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/.test(
|
|
7
|
+
obj
|
|
8
|
+
)
|
|
9
|
+
) {
|
|
10
|
+
return new Date(obj);
|
|
11
|
+
} else if (isArray(obj)) {
|
|
12
|
+
return obj.map((i) => me(i));
|
|
13
|
+
} else if (isObject(obj) && !isArray(obj)) {
|
|
14
|
+
const n = {};
|
|
15
|
+
Object.keys(obj).forEach((k) => (n[convert(k)] = me(obj[k])));
|
|
16
|
+
return n;
|
|
17
|
+
} else {
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import compression from "compression";
|
|
4
|
+
import helmet from "helmet";
|
|
5
|
+
import morgan from "morgan";
|
|
6
|
+
import rateLimit from "express-rate-limit";
|
|
7
|
+
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
const DEVELOPMENT = "development";
|
|
12
|
+
const { NODE_ENV = DEVELOPMENT, WEBPACK_URL = "http://localhost:8080" } =
|
|
13
|
+
process.env;
|
|
14
|
+
|
|
15
|
+
export default function createAppServer(opts = { port: 3000 }) {
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
|
|
19
|
+
const limiter = rateLimit({
|
|
20
|
+
windowMs: 15 * 60 * 1000, // X minutes
|
|
21
|
+
max: 100, // limit each IP to Y requests per windowMs
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.use(limiter);
|
|
25
|
+
|
|
26
|
+
if (process.env.NODE_ENV !== "test") {
|
|
27
|
+
app.use(morgan("combined"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (process.env.NODE_ENV === "production") {
|
|
31
|
+
app.use(compression());
|
|
32
|
+
app.use(helmet());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (NODE_ENV !== DEVELOPMENT) {
|
|
36
|
+
app.use(express.static("./dist"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (NODE_ENV === DEVELOPMENT && WEBPACK_URL) {
|
|
40
|
+
app.get(
|
|
41
|
+
"/*.js",
|
|
42
|
+
createProxyMiddleware({
|
|
43
|
+
target: WEBPACK_URL,
|
|
44
|
+
changeOrigin: true,
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
app.get(
|
|
49
|
+
"/",
|
|
50
|
+
createProxyMiddleware({
|
|
51
|
+
target: WEBPACK_URL,
|
|
52
|
+
changeOrigin: true,
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
app.get("/health", (req, res) => {
|
|
58
|
+
res.json({
|
|
59
|
+
success: true,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const server = app.listen(opts.port);
|
|
64
|
+
|
|
65
|
+
/* eslint-disable no-console */
|
|
66
|
+
console.log("Starting in %s mode", NODE_ENV);
|
|
67
|
+
console.log(
|
|
68
|
+
"Listening at http://%s:%s",
|
|
69
|
+
server.address().address === "::" ? "localhost" : server.address().address,
|
|
70
|
+
server.address().port
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return app;
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { camelCase, isArray, isDate } from "lodash-es";
|
|
2
|
+
import convertCase from "./convertCase.js";
|
|
3
|
+
|
|
4
|
+
export default function formatOutput(obj) {
|
|
5
|
+
if (isDate(obj)) {
|
|
6
|
+
return obj.toISOString();
|
|
7
|
+
}
|
|
8
|
+
if (isArray(obj)) {
|
|
9
|
+
return obj.map((v) => formatOutput(v));
|
|
10
|
+
}
|
|
11
|
+
return convertCase(obj, formatOutput, camelCase);
|
|
12
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as createAppServer } from "./createAppServer.js";
|
|
2
|
+
export { default as convertCase } from "./convertCase.js";
|
|
3
|
+
export { default as formatInput } from "./formatInput.js";
|
|
4
|
+
export { default as formatOutput } from "./formatOutput.js";
|
|
5
|
+
export * from "./asyncJsonHandler.js";
|