Repository: RobinTail/express-zod-api
Pull request #7 (2021-05-01 18:01:46)

Custom app


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66bac643..8b6793f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,35 @@
 
 ## Version 0
 
-### v0.4.2
-- Dataflow diagram improvement
+### v0.5.0
+- More convenient way to attach routing to your custom express app
+- `ConfigType` changes:
+```typescript
+// before
+export interface ConfigType {
+  server: {
+    listen: number | string;
+    cors: boolean;
+    jsonParser?: NextHandleFunction;
+    resultHandler?: ResultHandler;
+  },
+  logger: LoggerConfig | winston.Logger;
+}
+
+// after
+export type ConfigType = ({
+  server: { // server configuration
+    listen: number | string; // preserved
+    jsonParser?: NextHandleFunction; // preserved
+  },
+} | { // or your custom express app
+  app: Express
+}) & {
+  cors: boolean; // moved
+  resultHandler?: ResultHandler; // moved
+  logger: LoggerConfig | Logger;
+}
+```
 
 ### v0.4.1
 - Minor Readme file fixes and clarifications
diff --git a/README.md b/README.md
index f78b0c0f..230b562e 100644
--- a/README.md
+++ b/README.md
@@ -210,16 +210,21 @@ createServer(config, routing);
 
 ## Your custom server
 
-You can instantiate your own express app and connect your endpoints the following way:
+You can instantiate your own express app and connect your endpoints the following way. **Please note** that in this case you probably need to:
+- parse `request.body` yourself;
+- call `app.listen()` yourself;
+- handle `404` errors yourself;
 
 ```typescript
-import {ConfigType, createLogger, initRouting} from 'express-zod-api';
+import * as express from 'express';
+import {ConfigType, attachRouting} from 'express-zod-api';
 
-const config: ConfigType = {...};
-const logger = createLogger(config.logger);
+const app = express();
+const config: ConfigType = {app, ...};
 const routing = {...};
 
-initRouting({app, logger, config, routing});
+attachRouting(config, routing);
+app.listen();
 ```
 
 # Disclosing API specifications
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index ee14bc99..358c108e 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -1,7 +1,7 @@
 openapi: 3.0.0
 info:
   title: Example API
-  version: 0.4.1
+  version: 0.5.0
 paths:
   /v1/getUser:
     get:
diff --git a/example/index.ts b/example/index.ts
index 4b150928..aa65bb37 100644
--- a/example/index.ts
+++ b/example/index.ts
@@ -5,8 +5,8 @@ import {routing} from './routing';
 const config: ConfigType = {
   server: {
     listen: 8090,
-    cors: true
   },
+  cors: true,
   logger: {
     level: 'debug',
     color: true
diff --git a/package.json b/package.json
index 425c3559..4f9a746b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "express-zod-api",
-  "version": "0.4.1",
-  "description": "Express ZOD API",
+  "version": "0.5.0",
+  "description": "Express Zod API",
   "license": "MIT",
   "scripts": {
     "start": "ts-node example/index.ts",
diff --git a/src/config-type.ts b/src/config-type.ts
index c995088c..927089ea 100644
--- a/src/config-type.ts
+++ b/src/config-type.ts
@@ -1,5 +1,6 @@
 import {NextHandleFunction} from 'connect';
-import * as winston from 'winston';
+import {Express} from 'express';
+import {Logger} from 'winston';
 import {ResultHandler} from './result-handler';
 
 export const loggerLevels = {
@@ -13,18 +14,21 @@ export interface LoggerConfig {
   color: boolean;
 }
 
-export interface ConfigType {
-  server: {
+export type ConfigType = ({
+  server: { // server configuration
     // port or socket
     listen: number | string;
-    // enable cross-origin resource sharing
-    // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
-    cors: boolean;
     // custom JSON parser, default: express.json()
-    jsonParser?: NextHandleFunction,
-    // custom handler for errors and output, default: defaultResultHandler()
-    resultHandler?: ResultHandler;
+    jsonParser?: NextHandleFunction;
   },
+} | { // or your custom express app
+  app: Express
+}) & {
+  // enable cross-origin resource sharing
+  // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+  cors: boolean;
+  // custom handler for errors and output, default: defaultResultHandler()
+  resultHandler?: ResultHandler;
   // logger configuration or your custom winston logger
-  logger: LoggerConfig | winston.Logger;
+  logger: LoggerConfig | Logger;
 }
diff --git a/src/endpoint.ts b/src/endpoint.ts
index b2750a04..b590b494 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -133,7 +133,7 @@ export class Endpoint<IN extends ObjectSchema, OUT extends ObjectSchema, mIN, OP
     initialInput: any,
     output: any
   }) {
-    const resultHandler = this.resultHandler || config.server.resultHandler || defaultResultHandler;
+    const resultHandler = this.resultHandler || config.resultHandler || defaultResultHandler;
     try {
       await resultHandler({
         error, output, request, response, logger,
@@ -153,7 +153,7 @@ export class Endpoint<IN extends ObjectSchema, OUT extends ObjectSchema, mIN, OP
   }) {
     let output: any;
     let error: Error | null = null;
-    if (config.server.cors) {
+    if (config.cors) {
       this.setupCorsHeaders(response);
     }
     if (request.method === 'OPTIONS') {
diff --git a/src/index.ts b/src/index.ts
index 297b4e9b..97b2522d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,8 +5,8 @@ export { ObjectSchema, FlatObject } from './helpers';
 export { createLogger } from './logger';
 export { createMiddleware } from './middleware';
 export { ResultHandler } from './result-handler';
-export { Routing, initRouting } from './routing';
-export { createServer } from './server';
+export { Routing } from './routing';
+export { createServer, attachRouting } from './server';
 export { OpenAPI } from './open-api';
 
 import { z } from 'zod';
diff --git a/src/server.ts b/src/server.ts
index 869496d2..0ae3eedd 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,4 +1,5 @@
 import * as express from 'express';
+import {Server} from 'http';
 import {ConfigType} from './config-type';
 import {isLoggerConfig} from './helpers';
 import {createLogger} from './logger';
@@ -6,10 +7,18 @@ import {defaultResultHandler} from './result-handler';
 import {initRouting, Routing} from './routing';
 import * as createHttpError from 'http-errors';
 
-export function createServer(config: ConfigType, routing: Routing) {
+type ConfigWithServer = Exclude<ConfigType, {app: any}>;
+type ConfigWithApp = Exclude<ConfigType, {server: any}>;
+
+export function attachRouting(config: ConfigWithApp, routing: Routing): void {
+  const logger = isLoggerConfig(config.logger) ? createLogger(config.logger) : config.logger;
+  return initRouting({app: config.app, routing, logger, config});
+}
+
+export function createServer(config: ConfigWithServer, routing: Routing): Server {
   const logger = isLoggerConfig(config.logger) ? createLogger(config.logger) : config.logger;
   const app = express();
-  const resultHandler = config.server.resultHandler || defaultResultHandler;
+  const resultHandler = config.resultHandler || defaultResultHandler;
 
   app.use([
     config.server.jsonParser || express.json(),


Test patch:
diff --git a/tests/system/system.spec.ts b/tests/system/system.spec.ts
index 05f0d6da..af95937f 100644
--- a/tests/system/system.spec.ts
+++ b/tests/system/system.spec.ts
@@ -47,8 +47,8 @@ describe('App', () => {
     server = createServer({
       server: {
         listen: 8055,
-        cors: true
       },
+      cors: true,
       logger: {
         level: 'silent',
         color: false
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index 2369a98d..214bf34c 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -74,7 +74,7 @@ describe('Endpoint', () => {
       await endpoint.execute({
         request: requestMock as Request,
         response: responseMock as any as Response,
-        config: configMock as ConfigType,
+        config: configMock as unknown as ConfigType,
         logger: loggerMock
       });
       expect(middlewareMock).toBeCalledTimes(1);
diff --git a/tests/unit/routing.spec.ts b/tests/unit/routing.spec.ts
index ff3deb22..b5dfd3a8 100644
--- a/tests/unit/routing.spec.ts
+++ b/tests/unit/routing.spec.ts
@@ -1,6 +1,7 @@
 import {Express, RequestHandler, Request, Response} from 'express';
 import {Logger} from 'winston';
-import {EndpointsFactory, z, Routing, initRouting, ConfigType} from '../../src';
+import {EndpointsFactory, z, Routing, ConfigType} from '../../src';
+import {initRouting} from '../../src/routing';
 
 let appMock: any;
 let loggerMock: any;
@@ -73,11 +74,7 @@ describe('Routing', () => {
 
     test('Should execute endpoints with right arguments', async () => {
       const handlerMock = jest.fn().mockImplementationOnce(() => ({result: true}));
-      const configMock = {
-        server: {
-          cors: true
-        }
-      };
+      const configMock = { cors: true };
       const setEndpoint = new EndpointsFactory().build({
         methods: ['post'],
         input: z.object({
diff --git a/tests/unit/server.spec.ts b/tests/unit/server.spec.ts
index 49f7b072..3ec6cba4 100644
--- a/tests/unit/server.spec.ts
+++ b/tests/unit/server.spec.ts
@@ -1,8 +1,10 @@
+import * as http from 'http';
+
 let appMock: ReturnType<typeof newAppMock>;
 const expressJsonMock = jest.fn();
 const newAppMock = () => ({
   use: jest.fn(),
-  listen: jest.fn((port, cb) => cb()),
+  listen: jest.fn((port, cb) => {cb && cb(); return new http.Server(); }),
   get: jest.fn(),
   post: jest.fn()
 });
@@ -14,7 +16,8 @@ const expressMock = jest.mock('express', () => {
   return returnFunction;
 });
 
-import {ConfigType, createServer, EndpointsFactory, z} from '../../src';
+import * as express from 'express'; // mocked above
+import {ConfigType, createServer, attachRouting, EndpointsFactory, z} from '../../src';
 
 describe('Server', () => {
   beforeEach(() => {
@@ -34,8 +37,8 @@ describe('Server', () => {
       const configMock: ConfigType = {
         server: {
           listen: 8054,
-          cors: true
         },
+        cors: true,
         logger: {
           level: 'warn',
           color: false
@@ -72,10 +75,10 @@ describe('Server', () => {
       const configMock = {
         server: {
           listen: 8054,
-          cors: true,
           jsonParser: jest.fn(),
-          resultHandler: jest.fn()
         },
+        cors: true,
+        resultHandler: jest.fn(),
         logger: {
           info: jest.fn()
         }
@@ -94,15 +97,16 @@ describe('Server', () => {
           })
         }
       };
-      createServer(configMock as unknown as ConfigType, routingMock);
+      const server = createServer(configMock as unknown as ConfigType & {server: any}, routingMock);
+      expect(server).toBeInstanceOf(http.Server);
       expect(appMock).toBeTruthy();
       expect(appMock.use).toBeCalledTimes(2);
       expect(Array.isArray(appMock.use.mock.calls[0][0])).toBeTruthy();
       expect(appMock.use.mock.calls[0][0][0]).toBe(configMock.server.jsonParser);
       expect(typeof appMock.use.mock.calls[1][0]).toBe('function');
-      expect(configMock.server.resultHandler).toBeCalledTimes(0);
+      expect(configMock.resultHandler).toBeCalledTimes(0);
       appMock.use.mock.calls[1][0]({method: 'get', path: '/v1/test'});
-      expect(configMock.server.resultHandler).toBeCalledTimes(1);
+      expect(configMock.resultHandler).toBeCalledTimes(1);
       expect(configMock.logger.info).toBeCalledTimes(1);
       expect(configMock.logger.info).toBeCalledWith('Listening 8054');
       expect(appMock.get).toBeCalledTimes(1);
@@ -113,4 +117,47 @@ describe('Server', () => {
       expect(appMock.listen.mock.calls[0][0]).toBe(8054);
     });
   });
+
+  describe('attachRouting()', () => {
+    test('should attach routing to the custom express app', () => {
+      const app = express();
+      expect(appMock).toBeTruthy();
+      const configMock = {
+        app,
+        cors: true,
+        resultHandler: jest.fn(),
+        logger: {
+          info: jest.fn()
+        }
+      };
+      const routingMock = {
+        v1: {
+          test: new EndpointsFactory().build({
+            methods: ['get', 'post'],
+            input: z.object({
+              n: z.number()
+            }),
+            output: z.object({
+              b: z.boolean()
+            }),
+            handler: jest.fn()
+          })
+        }
+      };
+      // noinspection JSVoidFunctionReturnValueUsed
+      const result = attachRouting(configMock as unknown as ConfigType & {app: any}, routingMock);
+      expect(result).toBe(undefined);
+      expect(appMock.use).toBeCalledTimes(0);
+      expect(configMock.resultHandler).toBeCalledTimes(0);
+      expect(configMock.logger.info).toBeCalledTimes(0);
+      expect(appMock.listen).toBeCalledTimes(0);
+      expect(appMock.get).toBeCalledTimes(1);
+      expect(appMock.get.mock.calls[0][0]).toBe('/v1/test');
+      expect(appMock.post).toBeCalledTimes(1);
+      expect(appMock.post.mock.calls[0][0]).toBe('/v1/test');
+      app.listen(8054);
+      expect(appMock.listen).toBeCalledTimes(1);
+      expect(appMock.listen.mock.calls[0][0]).toBe(8054);
+    });
+  });
 });