Repository: RobinTail/express-zod-api
Pull request #254 (2021-12-30 17:16:38)

Ability to serve static files.
The feature requested in the discussion
https://github.com/RobinTail/express-zod-api/discussions/249

```typescript
import {Routing, ServeStatic} from "express-zod-api";

const routing: Routing = {
  // path /public serves static files from ./assets
  public: new ServeStatic(path.join(__dirname, "assets"), {
    dotfiles: "deny",
    index: false,
    redirect: false,
  }),
};
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 784e3a9f..a5f73fbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
 
 ## Version 5
 
+### v5.2.0
+
+- Feature #254: the ability to configure routes for serving static files.
+  - Use `new ServeStatic()` with the same options as `express.static()`.
+  - You can find all options here: http://expressjs.com/en/4x/api.html#express.static
+
+```typescript
+import { Routing, ServeStatic } from "express-zod-api";
+import path from "path";
+
+const routing: Routing = {
+  // path /public serves static files from ./assets
+  public: new ServeStatic(path.join(__dirname, "assets"), {
+    dotfiles: "deny",
+    index: false,
+    redirect: false,
+  }),
+};
+```
+
 ### v5.1.0
 
 - No changes.
diff --git a/README.md b/README.md
index 2c8fb6d7..fb74baa3 100644
--- a/README.md
+++ b/README.md
@@ -35,12 +35,13 @@ Start your API server with I/O schema validation and custom middlewares in minut
    7. [Non-object response](#non-object-response) including file downloads
    8. [File uploads](#file-uploads)
    9. [Customizing logger](#customizing-logger)
-   10. [Usage with your own express app](#usage-with-your-own-express-app)
+   10. [Connect to your own express app](#connect-to-your-own-express-app)
    11. [Multiple schemas for one route](#multiple-schemas-for-one-route)
-   12. [Customizing input sources](#customizing-input-sources)
-   13. [Enabling HTTPS](#enabling-https)
-   14. [Informing the frontend about the API](#informing-the-frontend-about-the-api)
-   15. [Creating a documentation](#creating-a-documentation)
+   12. [Serving static files](#serving-static-files)
+   13. [Customizing input sources](#customizing-input-sources)
+   14. [Enabling HTTPS](#enabling-https)
+   15. [Informing the frontend about the API](#informing-the-frontend-about-the-api)
+   16. [Creating a documentation](#creating-a-documentation)
 5. [Additional hints](#additional-hints)
    1. [How to test endpoints](#how-to-test-endpoints)
    2. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
@@ -505,7 +506,7 @@ const logger = winston.createLogger({
 const config = createConfig({ logger /* ..., */ });
 ```
 
-## Usage with your own express app
+## Connect to your own express app
 
 If you already have your own configured express application, or you find the library settings not enough,
 you can connect your routing to the app instead of using `createServer()`.
@@ -554,6 +555,25 @@ const routing: Routing = {
 };
 ```
 
+## Serving static files
+
+In case you want your server to serve static files, you can use `new ServeStatic()` in `Routing` using the options
+similar to `express.static()`. These options you may find [here](http://expressjs.com/en/4x/api.html#express.static).
+
+```typescript
+import { Routing, ServeStatic } from "express-zod-api";
+import path from "path";
+
+const routing: Routing = {
+  // path /public serves static files from ./assets
+  public: new ServeStatic(path.join(__dirname, "assets"), {
+    dotfiles: "deny",
+    index: false,
+    redirect: false,
+  }),
+};
+```
+
 ## Customizing input sources
 
 You can customize the list of `request` properties that are combined into `input` that is being validated and available
diff --git a/example/assets/dataflow.svg b/example/assets/dataflow.svg
new file mode 120000
index 00000000..2c9691ae
--- /dev/null
+++ b/example/assets/dataflow.svg
@@ -0,0 +1,1 @@
+../../dataflow.svg
\ No newline at end of file
diff --git a/example/assets/logo.svg b/example/assets/logo.svg
new file mode 120000
index 00000000..e756466d
--- /dev/null
+++ b/example/assets/logo.svg
@@ -0,0 +1,1 @@
+../../logo.svg
\ No newline at end of file
diff --git a/example/routing.ts b/example/routing.ts
index f8f19167..d7b43cdf 100644
--- a/example/routing.ts
+++ b/example/routing.ts
@@ -1,9 +1,10 @@
-import { DependsOnMethod, Routing } from "../src";
+import { DependsOnMethod, Routing, ServeStatic } from "../src";
 import { uploadAvatarEndpoint } from "./endpoints/upload-avatar";
 import { retrieveUserEndpoint } from "./endpoints/retrieve-user";
 import { sendAvatarEndpoint } from "./endpoints/send-avatar";
 import { updateUserEndpoint } from "./endpoints/update-user";
 import { streamAvatarEndpoint } from "./endpoints/stream-avatar";
+import path from "path";
 
 export const routing: Routing = {
   v1: {
@@ -23,4 +24,10 @@ export const routing: Routing = {
       upload: uploadAvatarEndpoint,
     },
   },
+  // path /public serves static files from /example/assets
+  public: new ServeStatic(path.join(__dirname, "assets"), {
+    dotfiles: "deny",
+    index: false,
+    redirect: false,
+  }),
 };
diff --git a/src/depends-on-method.ts b/src/depends-on-method.ts
new file mode 100644
index 00000000..81c89362
--- /dev/null
+++ b/src/depends-on-method.ts
@@ -0,0 +1,37 @@
+import { Endpoint } from "./endpoint";
+import { DependsOnMethodError } from "./errors";
+import { Method } from "./method";
+
+export class DependsOnMethod {
+  constructor(
+    public readonly methods: {
+      [K in Method]?:
+        | Endpoint<any, any, any, any, K, any, any>
+        | Endpoint<any, any, any, any, Method, any, any>;
+    }
+  ) {
+    (Object.keys(methods) as (keyof typeof methods)[]).forEach((key) => {
+      if (key in methods) {
+        const endpointMethods = methods[key]?.getMethods() || [];
+        if (!endpointMethods.includes(key)) {
+          throw new DependsOnMethodError(
+            `The endpoint assigned to the '${key}' parameter must have at least this method in its specification.\n` +
+              "This error should prevent mistakes during the development process.\n" +
+              "Example:\n\n" +
+              `new ${this.constructor.name}({\n` +
+              `  ${key}: endpointsFactory.build({\n` +
+              `    methods: ['${key}', ` +
+              ((methods[key]?.getMethods() || [])
+                .map((m) => `'${m}'`)
+                .join(", ") || "...") +
+              "]\n" +
+              `    // or method: '${key}'\n` +
+              "    ...\n" +
+              "  })\n" +
+              "});\n"
+          );
+        }
+      }
+    });
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index c219ce84..9e6c56a3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,7 +12,9 @@ export { createApiResponse } from "./api-response";
 export { createLogger } from "./logger";
 export { createMiddleware } from "./middleware";
 export { createResultHandler, defaultResultHandler } from "./result-handler";
-export { Routing, DependsOnMethod } from "./routing";
+export { DependsOnMethod } from "./depends-on-method";
+export { ServeStatic } from "./serve-static";
+export { Routing } from "./routing";
 export { createServer, attachRouting } from "./server";
 export { OpenAPI } from "./open-api";
 export { OpenAPIError, DependsOnMethodError, RoutingError } from "./errors";
diff --git a/src/open-api.ts b/src/open-api.ts
index 181f8054..acf76ec2 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -32,7 +32,11 @@ export class OpenAPI extends OpenApiBuilder {
   }: GeneratorParams) {
     super();
     this.addInfo({ title, version }).addServer({ url: serverUrl });
-    const cb: RoutingCycleParams["cb"] = (endpoint, path, _method) => {
+    const endpointCb: RoutingCycleParams["endpointCb"] = (
+      endpoint,
+      path,
+      _method
+    ) => {
       const method = _method as Method;
       const commonParams = { path, method, endpoint };
       const inputSources =
@@ -70,6 +74,6 @@ export class OpenAPI extends OpenApiBuilder {
         [method]: operation,
       });
     };
-    routingCycle({ routing, cb });
+    routingCycle({ routing, endpointCb });
   }
 }
diff --git a/src/routing.ts b/src/routing.ts
index f14aff7f..ccf58c6e 100644
--- a/src/routing.ts
+++ b/src/routing.ts
@@ -1,63 +1,33 @@
-import { Express } from "express";
+import { Express, static as _serveStatic } from "express";
 import { Logger } from "winston";
 import { CommonConfig } from "./config-type";
-import { AbstractEndpoint, Endpoint } from "./endpoint";
-import { DependsOnMethodError, RoutingError } from "./errors";
+import { DependsOnMethod } from "./depends-on-method";
+import { AbstractEndpoint } from "./endpoint";
+import { RoutingError } from "./errors";
 import { AuxMethod, Method } from "./method";
+import { ServeStatic, StaticHandler } from "./serve-static";
 import { getStartupLogo } from "./startup-logo";
 
-export class DependsOnMethod {
-  constructor(
-    public readonly methods: {
-      [K in Method]?:
-        | Endpoint<any, any, any, any, K, any, any>
-        | Endpoint<any, any, any, any, Method, any, any>;
-    }
-  ) {
-    (Object.keys(methods) as (keyof typeof methods)[]).forEach((key) => {
-      if (key in methods) {
-        const endpointMethods = methods[key]?.getMethods() || [];
-        if (!endpointMethods.includes(key)) {
-          throw new DependsOnMethodError(
-            `The endpoint assigned to the '${key}' parameter must have at least this method in its specification.\n` +
-              "This error should prevent mistakes during the development process.\n" +
-              "Example:\n\n" +
-              `new ${this.constructor.name}({\n` +
-              `  ${key}: endpointsFactory.build({\n` +
-              `    methods: ['${key}', ` +
-              ((methods[key]?.getMethods() || [])
-                .map((m) => `'${m}'`)
-                .join(", ") || "...") +
-              "]\n" +
-              `    // or method: '${key}'\n` +
-              "    ...\n" +
-              "  })\n" +
-              "});\n"
-          );
-        }
-      }
-    });
-  }
-}
-
 export interface Routing {
-  [SEGMENT: string]: Routing | DependsOnMethod | AbstractEndpoint;
+  [SEGMENT: string]: Routing | DependsOnMethod | AbstractEndpoint | ServeStatic;
 }
 
 export interface RoutingCycleParams {
   routing: Routing;
-  cb: (
+  endpointCb: (
     endpoint: AbstractEndpoint,
     path: string,
     method: Method | AuxMethod
   ) => void;
+  staticCb?: (path: string, handler: StaticHandler) => void;
   parentPath?: string;
   cors?: boolean;
 }
 
 export const routingCycle = ({
   routing,
-  cb,
+  endpointCb,
+  staticCb,
   parentPath,
   cors,
 }: RoutingCycleParams) => {
@@ -80,24 +50,29 @@ export const routingCycle = ({
         methods.push("options");
       }
       methods.forEach((method) => {
-        cb(element, path, method);
+        endpointCb(element, path, method);
       });
+    } else if (element instanceof ServeStatic) {
+      if (staticCb) {
+        staticCb(path, _serveStatic(...element.params));
+      }
     } else if (element instanceof DependsOnMethod) {
       Object.entries<AbstractEndpoint>(element.methods).forEach(
         ([method, endpoint]) => {
-          cb(endpoint, path, method as Method);
+          endpointCb(endpoint, path, method as Method);
         }
       );
       if (cors && Object.keys(element.methods).length > 0) {
         const firstEndpoint = Object.values(
           element.methods
         )[0] as AbstractEndpoint;
-        cb(firstEndpoint, path, "options");
+        endpointCb(firstEndpoint, path, "options");
       }
     } else {
       routingCycle({
         routing: element,
-        cb,
+        endpointCb,
+        staticCb,
         cors,
         parentPath: path,
       });
@@ -122,11 +97,14 @@ export const initRouting = ({
   routingCycle({
     routing,
     cors: config.cors,
-    cb: (endpoint, path, method) => {
+    endpointCb: (endpoint, path, method) => {
       app[method](path, async (request, response) => {
         logger.info(`${request.method}: ${path}`);
         await endpoint.execute({ request, response, logger, config });
       });
     },
+    staticCb: (path, handler) => {
+      app.use(path, handler);
+    },
   });
 };
diff --git a/src/serve-static.ts b/src/serve-static.ts
new file mode 100644
index 00000000..6b931d22
--- /dev/null
+++ b/src/serve-static.ts
@@ -0,0 +1,12 @@
+import { static as _serveStatic } from "express";
+
+type OriginalStatic = typeof _serveStatic;
+export type StaticHandler = ReturnType<OriginalStatic>;
+
+export class ServeStatic {
+  public params: Parameters<OriginalStatic>;
+
+  constructor(...params: Parameters<OriginalStatic>) {
+    this.params = params;
+  }
+}


Test patch:
diff --git a/tests/system/__snapshots__/example.spec.ts.snap b/tests/system/__snapshots__/example.spec.ts.snap
index ecabebdc..f0491613 100644
--- a/tests/system/__snapshots__/example.spec.ts.snap
+++ b/tests/system/__snapshots__/example.spec.ts.snap
@@ -18,8 +18,19 @@ Object {
 }
 `;
 
+exports[`Example Negative Should respond with error to the missing static file request 1`] = `
+Object {
+  "error": Object {
+    "message": "Can not GET /public/missing.svg",
+  },
+  "status": "error",
+}
+`;
+
 exports[`Example Positive Should send an image with a correct header 1`] = `"f39beeff92379dc935586d726211c2620be6f879"`;
 
+exports[`Example Positive Should serve static files 1`] = `"f39beeff92379dc935586d726211c2620be6f879"`;
+
 exports[`Example Positive Should stream an image with a correct header 1`] = `"f39beeff92379dc935586d726211c2620be6f879"`;
 
 exports[`Example Positive Should upload the file 1`] = `
diff --git a/tests/system/example.spec.ts b/tests/system/example.spec.ts
index acd01861..92c36d6e 100644
--- a/tests/system/example.spec.ts
+++ b/tests/system/example.spec.ts
@@ -131,6 +131,17 @@ describe("Example", () => {
       expect(hash).toMatchSnapshot();
     });
 
+    test("Should serve static files", async () => {
+      const response = await fetch("http://localhost:8090/public/logo.svg");
+      expect(response.status).toBe(200);
+      expect(response.headers.get("Content-type")).toBe("image/svg+xml");
+      const hash = crypto
+        .createHash("sha1")
+        .update(await response.text())
+        .digest("hex");
+      expect(hash).toMatchSnapshot();
+    });
+
     test("Should upload the file", async () => {
       const filename = "logo.svg";
       const logo = readFileSync(filename, "utf-8");
@@ -261,5 +272,14 @@ describe("Example", () => {
         },
       });
     });
+
+    test("Should respond with error to the missing static file request", async () => {
+      const response = await fetch("http://localhost:8090/public/missing.svg");
+      expect(response.status).toBe(404);
+      expect(response.headers.get("Content-type")).toBe(
+        "application/json; charset=utf-8"
+      );
+      expect(await response.json()).toMatchSnapshot();
+    });
   });
 });
diff --git a/tests/unit/__snapshots__/depends-on-method.spec.ts.snap b/tests/unit/__snapshots__/depends-on-method.spec.ts.snap
new file mode 100644
index 00000000..b71a951a
--- /dev/null
+++ b/tests/unit/__snapshots__/depends-on-method.spec.ts.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DependsOnMethod should throw an error if the endpoint does not have the corresponding method 1`] = `
+"The endpoint assigned to the 'post' parameter must have at least this method in its specification.
+This error should prevent mistakes during the development process.
+Example:
+
+new DependsOnMethod({
+  post: endpointsFactory.build({
+    methods: ['post', 'get', 'patch']
+    // or method: 'post'
+    ...
+  })
+});
+"
+`;
diff --git a/tests/unit/__snapshots__/routing.spec.ts.snap b/tests/unit/__snapshots__/routing.spec.ts.snap
index e335ec52..7d66ec22 100644
--- a/tests/unit/__snapshots__/routing.spec.ts.snap
+++ b/tests/unit/__snapshots__/routing.spec.ts.snap
@@ -1,20 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Routing DependsOnMethod should throw an error if the endpoint does not have the corresponding method 1`] = `
-"The endpoint assigned to the 'post' parameter must have at least this method in its specification.
-This error should prevent mistakes during the development process.
-Example:
-
-new DependsOnMethod({
-  post: endpointsFactory.build({
-    methods: ['post', 'get', 'patch']
-    // or method: 'post'
-    ...
-  })
-});
-"
-`;
-
 exports[`Routing initRouting() Should throw an error in case of slashes in route 1`] = `
 "Routing elements should not contain '/' character.
 The error caused by '/v1' route that has a 'user/retrieve' entry."
diff --git a/tests/unit/depends-on-method.spec.ts b/tests/unit/depends-on-method.spec.ts
new file mode 100644
index 00000000..cf09de6b
--- /dev/null
+++ b/tests/unit/depends-on-method.spec.ts
@@ -0,0 +1,59 @@
+import {
+  defaultResultHandler,
+  DependsOnMethod,
+  EndpointsFactory,
+  z,
+} from "../../src";
+
+describe("DependsOnMethod", () => {
+  test("should accept empty object", () => {
+    const instance = new DependsOnMethod({});
+    expect(instance).toBeInstanceOf(DependsOnMethod);
+    expect(instance.methods).toEqual({});
+  });
+
+  test("should accept an endpoint with a corresponding method", () => {
+    const instance = new DependsOnMethod({
+      post: new EndpointsFactory(defaultResultHandler).build({
+        method: "post",
+        input: z.object({}),
+        output: z.object({}),
+        handler: async () => ({}),
+      }),
+    });
+    expect(instance).toBeInstanceOf(DependsOnMethod);
+    expect(instance.methods).toHaveProperty("post");
+  });
+
+  test("should accept an endpoint with additional methods", () => {
+    const endpoint = new EndpointsFactory(defaultResultHandler).build({
+      methods: ["get", "post"],
+      input: z.object({}),
+      output: z.object({}),
+      handler: async () => ({}),
+    });
+    const instance = new DependsOnMethod({
+      get: endpoint,
+      post: endpoint,
+    });
+    expect(instance).toBeInstanceOf(DependsOnMethod);
+    expect(instance.methods).toHaveProperty("get");
+    expect(instance.methods).toHaveProperty("post");
+  });
+
+  test("should throw an error if the endpoint does not have the corresponding method", () => {
+    const endpoint = new EndpointsFactory(defaultResultHandler).build({
+      methods: ["get", "patch"],
+      input: z.object({}),
+      output: z.object({}),
+      handler: async () => ({}),
+    });
+    expect(
+      () =>
+        new DependsOnMethod({
+          get: endpoint,
+          post: endpoint,
+        })
+    ).toThrowErrorMatchingSnapshot();
+  });
+});
diff --git a/tests/unit/routing.spec.ts b/tests/unit/routing.spec.ts
index 38658672..34e0f905 100644
--- a/tests/unit/routing.spec.ts
+++ b/tests/unit/routing.spec.ts
@@ -1,3 +1,11 @@
+const staticHandler = jest.fn();
+const staticMock = jest.fn(() => staticHandler);
+
+jest.mock("express", () => ({
+  ...jest.requireActual("express"),
+  static: staticMock,
+}));
+
 import { Express, RequestHandler, Request, Response } from "express";
 import { Logger } from "winston";
 import {
@@ -6,6 +14,7 @@ import {
   Routing,
   DependsOnMethod,
   defaultResultHandler,
+  ServeStatic,
 } from "../../src";
 import { CommonConfig } from "../../src/config-type";
 import { mimeJson } from "../../src/mime";
@@ -24,6 +33,7 @@ describe("Routing", () => {
         delete: jest.fn(),
         patch: jest.fn(),
         options: jest.fn(),
+        use: jest.fn(),
       };
 
       loggerMock = {
@@ -72,7 +82,7 @@ describe("Routing", () => {
         app: appMock as Express,
         logger: loggerMock as Logger,
         config: configMock as CommonConfig,
-        routing: routing,
+        routing,
       });
       expect(appMock.get).toBeCalledTimes(2);
       expect(appMock.post).toBeCalledTimes(2);
@@ -89,6 +99,25 @@ describe("Routing", () => {
       expect(appMock.options.mock.calls[2][0]).toBe("/v1/user/universal");
     });
 
+    test("Should accept serveStatic", () => {
+      const routing: Routing = {
+        public: new ServeStatic(__dirname, { dotfiles: "deny" }),
+      };
+      const configMock = {
+        cors: true,
+        startupLogo: false,
+      };
+      initRouting({
+        app: appMock,
+        logger: loggerMock,
+        config: configMock as CommonConfig,
+        routing,
+      });
+      expect(staticMock).toHaveBeenCalledWith(__dirname, { dotfiles: "deny" });
+      expect(appMock.use).toHaveBeenCalledTimes(1);
+      expect(appMock.use).toHaveBeenCalledWith("/public", staticHandler);
+    });
+
     test("Should accept DependsOnMethod", () => {
       const handlerMock = jest.fn();
       const configMock = {
@@ -128,7 +157,7 @@ describe("Routing", () => {
         app: appMock as Express,
         logger: loggerMock as Logger,
         config: configMock as CommonConfig,
-        routing: routing,
+        routing,
       });
       expect(appMock.get).toBeCalledTimes(1);
       expect(appMock.post).toBeCalledTimes(1);
@@ -163,7 +192,7 @@ describe("Routing", () => {
         app: appMock as Express,
         logger: loggerMock as Logger,
         config: configMock as CommonConfig,
-        routing: routing,
+        routing,
       });
       expect(appMock.get).toBeCalledTimes(1);
       expect(appMock.get.mock.calls[0][0]).toBe("/v1/user/:id");
@@ -192,7 +221,7 @@ describe("Routing", () => {
         app: appMock as Express,
         logger: loggerMock as Logger,
         config: configMock as CommonConfig,
-        routing: routing,
+        routing,
       });
       expect(appMock.get).toBeCalledTimes(2);
       expect(appMock.get.mock.calls[0][0]).toBe("/v1/user/:id");
@@ -258,7 +287,7 @@ describe("Routing", () => {
         app: appMock as Express,
         logger: loggerMock as Logger,
         config: configMock as CommonConfig,
-        routing: routing,
+        routing,
       });
       expect(appMock.post).toBeCalledTimes(1);
       const routeHandler = appMock.post.mock.calls[0][1] as RequestHandler;
@@ -300,57 +329,4 @@ describe("Routing", () => {
       });
     });
   });
-
-  describe("DependsOnMethod", () => {
-    test("should accept empty object", () => {
-      const instance = new DependsOnMethod({});
-      expect(instance).toBeInstanceOf(DependsOnMethod);
-      expect(instance.methods).toEqual({});
-    });
-
-    test("should accept an endpoint with a corresponding method", () => {
-      const instance = new DependsOnMethod({
-        post: new EndpointsFactory(defaultResultHandler).build({
-          method: "post",
-          input: z.object({}),
-          output: z.object({}),
-          handler: async () => ({}),
-        }),
-      });
-      expect(instance).toBeInstanceOf(DependsOnMethod);
-      expect(instance.methods).toHaveProperty("post");
-    });
-
-    test("should accept an endpoint with additional methods", () => {
-      const endpoint = new EndpointsFactory(defaultResultHandler).build({
-        methods: ["get", "post"],
-        input: z.object({}),
-        output: z.object({}),
-        handler: async () => ({}),
-      });
-      const instance = new DependsOnMethod({
-        get: endpoint,
-        post: endpoint,
-      });
-      expect(instance).toBeInstanceOf(DependsOnMethod);
-      expect(instance.methods).toHaveProperty("get");
-      expect(instance.methods).toHaveProperty("post");
-    });
-
-    test("should throw an error if the endpoint does not have the corresponding method", () => {
-      const endpoint = new EndpointsFactory(defaultResultHandler).build({
-        methods: ["get", "patch"],
-        input: z.object({}),
-        output: z.object({}),
-        handler: async () => ({}),
-      });
-      expect(
-        () =>
-          new DependsOnMethod({
-            get: endpoint,
-            post: endpoint,
-          })
-      ).toThrowErrorMatchingSnapshot();
-    });
-  });
 });
diff --git a/tests/unit/serve-static.spec.ts b/tests/unit/serve-static.spec.ts
new file mode 100644
index 00000000..3462cae2
--- /dev/null
+++ b/tests/unit/serve-static.spec.ts
@@ -0,0 +1,11 @@
+import { ServeStatic } from "../../src";
+
+describe("ServeStatic", () => {
+  describe("constructor()", () => {
+    test("should create an instance that provides original params", () => {
+      const serverStatic = new ServeStatic(__dirname, { dotfiles: "deny" });
+      expect(serverStatic).toBeInstanceOf(ServeStatic);
+      expect(serverStatic.params).toEqual([__dirname, { dotfiles: "deny" }]);
+    });
+  });
+});