@stanlemon/server 0.2.60 → 0.3.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Express App Server
2
2
 
3
+ [![npm version](https://badge.fury.io/js/%40stanlemon%2Fserver.svg)](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
@@ -35,3 +35,5 @@ app.post(
35
35
  // You can also use async/await
36
36
  handler(async ({ name }) => await Promise.resolve({ hello: name || "Stan" }))
37
37
  );
38
+
39
+ app.catch404s();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stanlemon/server",
3
- "version": "0.2.60",
3
+ "version": "0.3.0",
4
4
  "description": "A basic express web server setup.",
5
5
  "author": "Stan Lemon <stanlemon@users.noreply.github.com>",
6
6
  "license": "MIT",
@@ -13,14 +13,16 @@
13
13
  "scripts": {
14
14
  "start": "NODE_ENV=development nodemon ./app.js",
15
15
  "lint": "eslint --ext js,jsx,ts,tsx ./",
16
- "lint:format": "eslint --fix --ext js,jsx,ts,tsx ./",
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",
@@ -34,6 +36,6 @@
34
36
  "devDependencies": {
35
37
  "@stanlemon/eslint-config": "*",
36
38
  "@types/lodash": "^4.14.202",
37
- "nodemon": "^3.0.1"
39
+ "nodemon": "^3.0.2"
38
40
  }
39
41
  }
@@ -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 /*, next */) => {
9
- const input = buildInput(req);
10
-
16
+ return async (req, res, next) => {
11
17
  try {
12
- const output = await fn(input);
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
- const body = formatInput(req.body);
58
-
59
- // This is a weird payload, don't try any of our append magic
60
- if (!isPlainObject(body)) {
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) {
@@ -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.json({ strict: false }));
41
+ app.use(express.urlencoded({ extended: true }));
42
+ app.use(express.json());
43
+ app.use(cookieParser());
18
44
 
19
- if (process.env.NODE_ENV !== "test") {
45
+ if (NODE_ENV !== "test") {
20
46
  app.use(morgan("combined"));
21
47
  }
22
48
 
23
- if (process.env.NODE_ENV === "production") {
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
- if (
37
- webpack !== false &&
38
- process.env.NODE_ENV !== "production" &&
39
- process.env.NODE_ENV !== "test"
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
- "/*.js",
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.get("/health", (req, res) => {
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
- /* eslint-disable no-console */
80
- console.log("Starting in %s mode", process.env.NODE_ENV);
81
- console.log(
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;
@@ -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
- res.status(400).json({
37
- errors: Object.assign.apply(
38
- null,
39
- error.details.map((d) => ({ [d.path]: d.message }))
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