@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 ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "@stanlemon"
4
+ ]
5
+ }
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,6 @@
1
+ import { snakeCase } from "lodash-es";
2
+ import convertCase from "./convertCase.js";
3
+
4
+ export default function formatInput(obj) {
5
+ return convertCase(obj, formatInput, snakeCase);
6
+ }
@@ -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";