{"id":14957650,"url":"https://github.com/robintail/express-zod-api","last_synced_at":"2026-02-03T10:04:31.278Z","repository":{"id":37038157,"uuid":"339881031","full_name":"RobinTail/express-zod-api","owner":"RobinTail","description":"A Typescript framework to help you get an API server up and running with I/O schema validation and custom middlewares in minutes.","archived":false,"fork":false,"pushed_at":"2026-01-28T00:47:15.000Z","size":17429,"stargazers_count":795,"open_issues_count":3,"forks_count":35,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-01-28T16:07:00.759Z","etag":null,"topics":["api","documentation","documentation-tool","endpoint","express","hacktoberfest","http","json","middleware","nodejs","openapi","openapi-specification","schema","schema-validation","server","swagger","swagger-documentation","typescript","validation","zod"],"latest_commit_sha":null,"homepage":"https://ez.robintail.cz","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RobinTail.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"RobinTail","buy_me_a_coffee":"robintail"}},"created_at":"2021-02-17T23:16:49.000Z","updated_at":"2026-01-28T14:06:52.000Z","dependencies_parsed_at":"2023-12-18T23:27:28.866Z","dependency_job_id":"4d11cac8-0577-40cc-8355-95d7c27749e4","html_url":"https://github.com/RobinTail/express-zod-api","commit_stats":{"total_commits":2576,"total_committers":11,"mean_commits":234.1818181818182,"dds":0.5213509316770186,"last_synced_commit":"63ea6e900ec54eaf1ed82564d3d81e160e769c96"},"previous_names":[],"tags_count":593,"template":false,"template_full_name":null,"purl":"pkg:github/RobinTail/express-zod-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobinTail%2Fexpress-zod-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobinTail%2Fexpress-zod-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobinTail%2Fexpress-zod-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobinTail%2Fexpress-zod-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RobinTail","download_url":"https://codeload.github.com/RobinTail/express-zod-api/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RobinTail%2Fexpress-zod-api/sbom","scorecard":{"id":121876,"data":{"date":"2025-08-11","repo":{"name":"github.com/RobinTail/express-zod-api","commit":"84e559243e2ab1dc8f64766f07f6ea571212fabc"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":6.1,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/17 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql-analysis.yml:28","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yml:29","Warn: topLevel 'contents' permission set to 'write': .github/workflows/bump.yml:30","Warn: no topLevel permission defined: .github/workflows/codeql-analysis.yml:1","Warn: topLevel 'contents' permission set to 'write': .github/workflows/dependencies.yml:9","Warn: topLevel 'contents' permission set to 'write': .github/workflows/headers.yml:9","Warn: no topLevel permission defined: .github/workflows/node.js.yml:1","Warn: no topLevel permission defined: .github/workflows/npm-publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/bump.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/bump.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/bump.yml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/bump.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/bump.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/bump.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/bump.yml:44: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/bump.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:42: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/codeql-analysis.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:46: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/codeql-analysis.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:57: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/codeql-analysis.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/codeql-analysis.yml:71: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/codeql-analysis.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/dependencies.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/dependencies.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/dependencies.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/dependencies.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/dependencies.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/dependencies.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/dependencies.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/dependencies.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/headers.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/headers.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/headers.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/headers.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/headers.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/headers.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/headers.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/headers.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/headers.yml:54: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/headers.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/node.js.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/node.js.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/node.js.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/node.js.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/node.js.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/node.js.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/node.js.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/node.js.yml:50: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/node.js.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/npm-publish.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:33: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/npm-publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm-publish.yml:34: update your workflow using https://app.stepsecurity.io/secureworkflow/RobinTail/express-zod-api/npm-publish.yml/master?enable=pin","Info:   0 out of  14 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of  11 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"SAST","score":9,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 20 commits out of 21 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-16T02:37:57.893Z","repository_id":37038157,"created_at":"2025-08-16T02:37:57.893Z","updated_at":"2025-08-16T02:37:57.893Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29040801,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-03T09:57:37.951Z","status":"ssl_error","status_checked_at":"2026-02-03T09:55:14.920Z","response_time":96,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","documentation","documentation-tool","endpoint","express","hacktoberfest","http","json","middleware","nodejs","openapi","openapi-specification","schema","schema-validation","server","swagger","swagger-documentation","typescript","validation","zod"],"created_at":"2024-09-24T13:15:17.504Z","updated_at":"2026-02-03T10:04:31.267Z","avatar_url":"https://github.com/RobinTail.png","language":"TypeScript","readme":"# Express Zod API\n\n![logo](https://raw.githubusercontent.com/RobinTail/express-zod-api/master/logo.svg)\n\n![CI](https://github.com/RobinTail/express-zod-api/actions/workflows/node.js.yml/badge.svg)\n![OpenAPI](https://img.shields.io/swagger/valid/3.0?specUrl=https%3A%2F%2Fraw.githubusercontent.com%2FRobinTail%2Fexpress-zod-api%2Fmaster%2Fexample%2Fexample.documentation.yaml\u0026label=OpenAPI)\n[![coverage](https://coveralls.io/repos/github/RobinTail/express-zod-api/badge.svg)](https://coveralls.io/github/RobinTail/express-zod-api)\n\n![downloads](https://img.shields.io/npm/dw/express-zod-api.svg)\n![npm release](https://img.shields.io/npm/v/express-zod-api.svg?color=green25\u0026label=latest)\n![GitHub Repo stars](https://img.shields.io/github/stars/RobinTail/express-zod-api.svg?style=flat)\n![License](https://img.shields.io/npm/l/express-zod-api.svg?color=green25)\n\nStart your API server with I/O schema validation and custom middlewares in minutes.\n\n1. [Overview](#overview)\n2. [How it works](#how-it-works)\n3. [Quick start](#quick-start) — **Fast Track**\n4. [Basic features](#basic-features)\n   1. [Routing](#routing) including static file serving\n   2. [Middlewares](#middlewares)\n   3. [Context](#context)\n   4. [Using native express middlewares](#using-native-express-middlewares)\n   5. [Refinements](#refinements)\n   6. [Query string parser](#query-string-parser)\n   7. [Transformations](#transformations)\n   8. [Top level transformations and mapping](#top-level-transformations-and-mapping)\n   9. [Dealing with dates](#dealing-with-dates)\n   10. [Cross-Origin Resource Sharing](#cross-origin-resource-sharing) (CORS)\n   11. [Enabling HTTPS](#enabling-https)\n   12. [Enabling compression](#enabling-compression)\n   13. [Customizing logger](#customizing-logger)\n   14. [Child logger](#child-logger)\n5. [Advanced features](#advanced-features)\n   1. [Customizing input sources](#customizing-input-sources)\n   2. [Headers as input source](#headers-as-input-source)\n   3. [Response customization](#response-customization)\n   4. [Empty response](#empty-response)\n   5. [Non-JSON response](#non-json-response) including file downloads\n   6. [Error handling](#error-handling)\n   7. [Production mode](#production-mode)\n   8. [HTML Forms (URL encoded)](#html-forms-url-encoded)\n   9. [File uploads](#file-uploads)\n   10. [Connect to your own express app](#connect-to-your-own-express-app)\n   11. [Testing endpoints](#testing-endpoints)\n   12. [Testing middlewares](#testing-middlewares)\n6. [Integration and Documentation](#integration-and-documentation)\n   1. [Zod Plugin](#zod-plugin)\n   2. [Generating a Frontend Client](#generating-a-frontend-client)\n   3. [Creating a documentation](#creating-a-documentation)\n   4. [Tagging the endpoints](#tagging-the-endpoints)\n   5. [Deprecated schemas and routes](#deprecated-schemas-and-routes)\n   6. [Customizable brands handling](#customizable-brands-handling)\n7. [Special needs](#special-needs)\n   1. [Different responses for different status codes](#different-responses-for-different-status-codes)\n   2. [Array response](#array-response) for migrating legacy APIs\n   3. [Accepting raw data](#accepting-raw-data)\n   4. [Profiling](#profiling)\n   5. [Graceful shutdown](#graceful-shutdown)\n   6. [Subscriptions](#subscriptions)\n8. [Caveats](#caveats)\n   1. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)\n9. [Your input to my output](#your-input-to-my-output)\n\nSee also [Changelog](CHANGELOG.md) and [automated migration](https://www.npmjs.com/package/@express-zod-api/migration).\n\n# Overview\n\nI made this framework because of the often repetitive tasks of starting a web server APIs with the need to validate input\ndata. It integrates and provides the capabilities of popular web server, logging, validation and documenting solutions.\nTherefore, many basic tasks can be accomplished faster and easier, in particular:\n\n- You can describe web server routes as a hierarchical object.\n- You can keep the endpoint's input and output type declarations right next to its handler.\n- All input and output data types are validated, so it ensures you won't have an empty string, null or undefined where\n  you expect a number.\n- Variables within an endpoint handler have types according to the declared schema, so your IDE and TypeScript will\n  provide you with necessary hints to focus on bringing your vision to life.\n- All of your endpoints can respond in a consistent way.\n- The expected endpoint input and response types can be exported to the frontend, so you don't get confused about the\n  field names when you implement the client for your API.\n- You can generate your API documentation in OpenAPI 3.1 and JSON Schema compatible format.\n\n## Contributors\n\nThese people contributed to the improvement of the framework by reporting bugs, making changes and suggesting ideas:\n\n[\u003cimg src=\"https://github.com/pycanis.png\" alt=\"@pycanis\" width=\"50\" /\u003e](https://github.com/pycanis)\n[\u003cimg src=\"https://github.com/arlyon.png\" alt=\"@arlyon\" width=\"50\" /\u003e](https://github.com/arlyon)\n[\u003cimg src=\"https://github.com/Upsilon-Iridani.png\" alt=\"@Upsilon-Iridani\" width=\"50\" /\u003e](https://github.com/Upsilon-Iridani)\n[\u003cimg src=\"https://github.com/NicolasMahe.png\" alt=\"@NicolasMahe\" width=\"50\" /\u003e](https://github.com/NicolasMahe)\n[\u003cimg src=\"https://github.com/shadone.png\" alt=\"@shadone\" width=\"50\" /\u003e](https://github.com/shadone)\n[\u003cimg src=\"https://github.com/squishykid.png\" alt=\"@squishykid\" width=\"50\" /\u003e](https://github.com/squishykid)\n[\u003cimg src=\"https://github.com/jakub-msqt.png\" alt=\"@jakub-msqt\" width=\"50\" /\u003e](https://github.com/jakub-msqt)\n[\u003cimg src=\"https://github.com/misha-z1nchuk.png\" alt=\"@misha-z1nchuk\" width=\"50\" /\u003e](https://github.com/misha-z1nchuk)\n[\u003cimg src=\"https://github.com/GreaterTamarack.png\" alt=\"@GreaterTamarack\" width=\"50\" /\u003e](https://github.com/GreaterTamarack)\n[\u003cimg src=\"https://github.com/pepegc.png\" alt=\"@pepegc\" width=\"50\" /\u003e](https://github.com/pepegc)\n[\u003cimg src=\"https://github.com/MichaelHindley.png\" alt=\"@MichaelHindley\" width=\"50\" /\u003e](https://github.com/MichaelHindley)\n[\u003cimg src=\"https://github.com/zoton2.png\" alt=\"@zoton2\" width=\"50\" /\u003e](https://github.com/zoton2)\n[\u003cimg src=\"https://github.com/ThomasKientz.png\" alt=\"@ThomasKientz\" width=\"50\" /\u003e](https://github.com/ThomasKientz)\n[\u003cimg src=\"https://github.com/james10424.png\" alt=\"@james10424\" width=\"50\" /\u003e](https://github.com/james10424)\n[\u003cimg src=\"https://github.com/HeikoOsigus.png\" alt=\"@HeikoOsigus\" width=\"50\" /\u003e](https://github.com/HeikoOsigus)\n[\u003cimg src=\"https://github.com/crgeary.png\" alt=\"@crgeary\" width=\"50\" /\u003e](https://github.com/crgeary)\n[\u003cimg src=\"https://github.com/williamgcampbell.png\" alt=\"@williamgcampbell\" width=\"50\" /\u003e](https://github.com/williamgcampbell)\n[\u003cimg src=\"https://github.com/gmorgen1.png\" alt=\"@gmorgen1\" width=\"50\" /\u003e](https://github.com/gmorgen1)\n[\u003cimg src=\"https://github.com/danmichaelo.png\" alt=\"@danmichaelo\" width=\"50\" /\u003e](https://github.com/danmichaelo)\n[\u003cimg src=\"https://github.com/APTy.png\" alt=\"@APTy\" width=\"50\" /\u003e](https://github.com/APTy)\n[\u003cimg src=\"https://github.com/LufyCZ.png\" alt=\"@LufyCZ\" width=\"50\" /\u003e](https://github.com/LufyCZ)\n[\u003cimg src=\"https://github.com/mlms13.png\" alt=\"@mlms13\" width=\"50\" /\u003e](https://github.com/mlms13)\n[\u003cimg src=\"https://github.com/bobgubko.png\" alt=\"@bobgubko\" width=\"50\" /\u003e](https://github.com/bobgubko)\n[\u003cimg src=\"https://github.com/LucWag.png\" alt=\"@LucWag\" width=\"50\" /\u003e](https://github.com/LucWag)\n[\u003cimg src=\"https://github.com/HenriJ.png\" alt=\"@HenriJ\" width=\"50\" /\u003e](https://github.com/HenriJ)\n[\u003cimg src=\"https://github.com/JonParton.png\" alt=\"@JonParton\" width=\"50\" /\u003e](https://github.com/JonParton)\n[\u003cimg src=\"https://github.com/t1nky.png\" alt=\"@t1nky\" width=\"50\" /\u003e](https://github.com/t1nky)\n[\u003cimg src=\"https://github.com/Tomtec331.png\" alt=\"@Tomtec331\" width=\"50\" /\u003e](https://github.com/Tomtec331)\n[\u003cimg src=\"https://github.com/rottmann.png\" alt=\"@rottmann\" width=\"50\" /\u003e](https://github.com/rottmann)\n[\u003cimg src=\"https://github.com/boarush.png\" alt=\"@boarush\" width=\"50\" /\u003e](https://github.com/boarush)\n[\u003cimg src=\"https://github.com/shawncarr.png\" alt=\"@shawncarr\" width=\"50\" /\u003e](https://github.com/shawncarr)\n[\u003cimg src=\"https://github.com/ben-xD.png\" alt=\"@ben-xD\" width=\"50\" /\u003e](https://github.com/ben-xD)\n[\u003cimg src=\"https://github.com/daniel-white.png\" alt=\"@daniel-white\" width=\"50\" /\u003e](https://github.com/daniel-white)\n[\u003cimg src=\"https://github.com/kotsmile.png\" alt=\"@kotsmile\" width=\"50\" /\u003e](https://github.com/kotsmile)\n[\u003cimg src=\"https://github.com/elee1766.png\" alt=\"@elee1766\" width=\"50\" /\u003e](https://github.com/elee1766)\n[\u003cimg src=\"https://github.com/danclaytondev.png\" alt=\"@danclaytondev\" width=\"50\" /\u003e](https://github.com/danclaytondev)\n[\u003cimg src=\"https://github.com/huyhoang160593.png\" alt=\"@huyhoang160593\" width=\"50\" /\u003e](https://github.com/huyhoang160593)\n[\u003cimg src=\"https://github.com/sarahssharkey.png\" alt=\"@sarahssharkey\" width=\"50\" /\u003e](https://github.com/sarahssharkey)\n[\u003cimg src=\"https://github.com/master-chu.png\" alt=\"@master-chu\" width=\"50\" /\u003e](https://github.com/master-chu)\n[\u003cimg src=\"https://github.com/alindsay55661.png\" alt=\"@alindsay55661\" width=\"50\" /\u003e](https://github.com/alindsay55661)\n[\u003cimg src=\"https://github.com/john-schmitz.png\" alt=\"@john-schmitz\" width=\"50\" /\u003e](https://github.com/john-schmitz)\n[\u003cimg src=\"https://github.com/miki725.png\" alt=\"@miki725\" width=\"50\" /\u003e](https://github.com/miki725)\n[\u003cimg src=\"https://github.com/dev-m1-macbook.png\" alt=\"@dev-m1-macbook\" width=\"50\" /\u003e](https://github.com/dev-m1-macbook)\n[\u003cimg src=\"https://github.com/McMerph.png\" alt=\"@McMerph\" width=\"50\" /\u003e](https://github.com/McMerph)\n[\u003cimg src=\"https://github.com/niklashigi.png\" alt=\"@niklashigi\" width=\"50\" /\u003e](https://github.com/niklashigi)\n[\u003cimg src=\"https://github.com/maxcohn.png\" alt=\"@maxcohn\" width=\"50\" /\u003e](https://github.com/maxcohn)\n[\u003cimg src=\"https://github.com/VideoSystemsTech.png\" alt=\"@VideoSystemsTech\" width=\"50\" /\u003e](https://github.com/VideoSystemsTech)\n[\u003cimg src=\"https://github.com/TheWisestOne.png\" alt=\"@TheWisestOne\" width=\"50\" /\u003e](https://github.com/TheWisestOne)\n[\u003cimg src=\"https://github.com/lazylace37.png\" alt=\"@lazylace37\" width=\"50\" /\u003e](https://github.com/lazylace37)\n[\u003cimg src=\"https://github.com/leosuncin.png\" alt=\"@leosuncin\" width=\"50\" /\u003e](https://github.com/leosuncin)\n[\u003cimg src=\"https://github.com/kirdk.png\" alt=\"@kirdk\" width=\"50\" /\u003e](https://github.com/kirdk)\n[\u003cimg src=\"https://github.com/johngeorgewright.png\" alt=\"@johngeorgewright\" width=\"50\" /\u003e](https://github.com/johngeorgewright)\n[\u003cimg src=\"https://github.com/ssteuteville.png\" alt=\"@ssteuteville\" width=\"50\" /\u003e](https://github.com/ssteuteville)\n[\u003cimg src=\"https://github.com/foxfirecodes.png\" alt=\"@foxfirecodes\" width=\"50\" /\u003e](https://github.com/foxfirecodes)\n[\u003cimg src=\"https://github.com/HardCoreQual.png\" alt=\"@HardCoreQual\" width=\"50\" /\u003e](https://github.com/HardCoreQual)\n[\u003cimg src=\"https://github.com/hellovai.png\" alt=\"@hellovai\" width=\"50\" /\u003e](https://github.com/hellovai)\n[\u003cimg src=\"https://github.com/Isaac-Leonard.png\" alt=\"@Isaac-Leonard\" width=\"50\" /\u003e](https://github.com/Isaac-Leonard)\n[\u003cimg src=\"https://github.com/digimuza.png\" alt=\"@digimuza\" width=\"50\" /\u003e](https://github.com/digimuza)\n[\u003cimg src=\"https://github.com/glitch452.png\" alt=\"@glitch452\" width=\"50\" /\u003e](https://github.com/glitch452)\n\n# How it works\n\n## Concept\n\nThe API operates object schemas for input and output validation.\nThe object being validated is the combination of certain `request` properties.\nIt is available to the endpoint handler as the `input` parameter.\nMiddlewares have access to all `request` properties, they can provide endpoints with `ctx` (context).\nThe object returned by the endpoint handler is called `output`. It goes to the `ResultHandler` which is\nresponsible for transmitting consistent responses containing the `output` or possible error.\nMuch can be customized to fit your needs.\n\n![Dataflow](https://raw.githubusercontent.com/RobinTail/express-zod-api/master/dataflow.svg)\n\n## Technologies\n\n- [Typescript](https://www.typescriptlang.org/) first.\n- Web server — [Express.js](https://expressjs.com/) v5.\n- Schema validation — [Zod 4.x](https://github.com/colinhacks/zod) including [Zod Plugin](#zod-plugin):\n  - For using with Zod 3.x install the framework versions below 24.0.0.\n- Supports any logger having `info()`, `debug()`, `error()` and `warn()` methods;\n  - Built-in console logger with colorful and pretty inspections by default.\n- Generators:\n  - Documentation — [OpenAPI 3.1](https://github.com/metadevpro/openapi3-ts) (former Swagger);\n  - Client side types — inspired by [zod-to-ts](https://github.com/sachinraja/zod-to-ts).\n- File uploads — [Express-FileUpload](https://github.com/richardgirges/express-fileupload)\n  (based on [Busboy](https://github.com/mscdex/busboy)).\n\n# Quick start\n\n## Installation\n\nInstall the framework, its peer dependencies and type assistance packages using your favorite\n[package manager](https://medium.com/@mahernaija/choosing-the-best-javascript-package-manager-in-2025-77b912ab3eda).\n\n```shell\n# example for pnpm:\npnpm add express-zod-api express zod http-errors\npnpm add -D @types/express @types/node @types/http-errors\n```\n\n## Environment preparation\n\nEnable the following `compilerOptions` in your `tsconfig.json` to make it work as expected:\n\n```json\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n## Set up config\n\nCreate a minimal configuration. Find out all configurable options\n[in sources](https://github.com/RobinTail/express-zod-api/blob/master/express-zod-api/src/config-type.ts).\n\n```ts\nimport { createConfig } from \"express-zod-api\";\n\nconst config = createConfig({\n  http: { listen: 8090 }, // port, UNIX socket or Net::ListenOptions\n  cors: false, // decide whether to enable CORS\n});\n```\n\n## Create your first endpoint\n\nUse the default factory to make an endpoint that responds with \"Hello, World\" or \"Hello, {name}\" depending on inputs.\nLearn how to make factories for [custom response](#response-customization) and by [adding middlewares](#middlewares).\n\n```ts\nimport { defaultEndpointsFactory } from \"express-zod-api\";\nimport { z } from \"zod\";\n\nconst helloWorldEndpoint = defaultEndpointsFactory.build({\n  // method: \"get\" (default) or array [\"get\", \"post\", ...]\n  input: z.object({\n    name: z.string().optional(),\n  }),\n  output: z.object({\n    greetings: z.string(),\n  }),\n  handler: async ({ input: { name }, ctx, logger }) =\u003e {\n    logger.debug(\"Context:\", ctx); // middlewares provide ctx\n    return { greetings: `Hello, ${name || \"World\"}. Happy coding!` };\n  },\n});\n```\n\n## Set up routing\n\nConnect your endpoint to the `/v1/hello` route:\n\n```ts\nimport { Routing } from \"express-zod-api\";\n\nconst routing: Routing = {\n  v1: {\n    hello: helloWorldEndpoint,\n  },\n};\n```\n\n## Create your server\n\nSee the [complete implementation example](https://github.com/RobinTail/express-zod-api/tree/master/example).\n\n```ts\nimport { createServer } from \"express-zod-api\";\n\ncreateServer(config, routing);\n```\n\n## Try it\n\nStart your application and execute the following command:\n\n```shell\ncurl -L -X GET 'localhost:8090/v1/hello?name=Rick'\n```\n\nYou should receive the following response:\n\n```json\n{ \"status\": \"success\", \"data\": { \"greetings\": \"Hello, Rick. Happy coding!\" } }\n```\n\n# Basic features\n\n## Routing\n\nThe framework offers flexible ways to define your routes, supporting both nested and flat syntaxes, dynamic path\nparameters, method-based routing, and static file serving. This example brings together all supported routing styles\nin one place, illustrating how you can structure your API using whichever method best fits your application’s\narchitecture — or even mix them seamlessly.\n\n```ts\nimport { Routing, ServeStatic } from \"express-zod-api\";\n\nconst routing: Routing = {\n  // flat syntax — /v1/users\n  \"/v1/users\": listUsersEndpoint,\n  // nested syntax\n  v1: {\n    // the way to have both — /v1/path and /v1/path/subpath\n    path: endpointA.nest({\n      subpath: endpointB,\n    }),\n    // path parameters — /v1/user/:id\n    user: {\n      \":id\": getUserEndpoint,\n    },\n    // mixed syntax with explicit method — /v1/user/:id\n    \"delete /user/:id\": deleteUserEndpoint,\n    // method-based routing — /v1/account\n    account: {\n      get: endpointA,\n      delete: endpointA,\n      post: endpointB,\n      patch: endpointB,\n    },\n  },\n  // static file serving — /public serves files from ./assets\n  public: new ServeStatic(\"assets\", {\n    /** @see https://expressjs.com/en/5x/api.html#express.static */\n    dotfiles: \"deny\",\n    index: false,\n    redirect: false,\n  }),\n};\n```\n\nSame Endpoint can be reused on different routes or handle multiple methods if needed. Path parameters (the `:id` above)\nshould be declared in the endpoint’s input schema. Properties assigned with Endpoint can explicitly declare a method.\nIf no method is specified, the methods supported by the endpoint are used (or `get` as a fallback).\n\n## Middlewares\n\nMiddleware can authenticate using input or `request` headers, and can provide endpoint handlers with `ctx`.\nInputs of middlewares are also available to endpoint handlers within `input`.\n\nHere is an example of the authentication middleware, that checks a `key` from input and `token` from headers:\n\n```ts\nimport { z } from \"zod\";\nimport createHttpError from \"http-errors\";\nimport { Middleware } from \"express-zod-api\";\n\nconst authMiddleware = new Middleware({\n  security: {\n    // this information is optional and used for generating documentation\n    and: [\n      { type: \"input\", name: \"key\" },\n      { type: \"header\", name: \"token\" },\n    ],\n  },\n  input: z.object({\n    key: z.string().min(1),\n  }),\n  handler: async ({ input: { key }, request, logger }) =\u003e {\n    logger.debug(\"Checking the key and token\");\n    const user = await db.Users.findOne({ key });\n    if (!user) throw createHttpError(401, \"Invalid key\");\n    if (request.headers.token !== user.token)\n      throw createHttpError(401, \"Invalid token\");\n    return { user }; // provides endpoints with ctx.user\n  },\n});\n```\n\nBy using `.addMiddleware()` method before `.build()` you can connect it to the endpoint:\n\n```ts\nconst yourEndpoint = defaultEndpointsFactory\n  .addMiddleware(authMiddleware)\n  .build({\n    handler: async ({ ctx: { user } }) =\u003e {\n      // user is the one returned by authMiddleware\n    }, // ...\n  });\n```\n\nYou can create a new factory by connecting as many middlewares as you want — they will be executed in the specified\norder for all the endpoints produced on that factory. You may also use a shorter inline syntax within the\n`.addMiddleware()` method, and have access to the output of the previously executed middlewares in chain as `ctx`:\n\n```ts\nimport { defaultEndpointsFactory } from \"express-zod-api\";\n\nconst factory = defaultEndpointsFactory\n  .addMiddleware(authMiddleware) // add Middleware instance or use shorter syntax:\n  .addMiddleware({\n    handler: async ({ ctx: { user } }) =\u003e ({}), // user from authMiddleware\n  });\n```\n\n## Context\n\nIf you need to provide your endpoints with a context that does not depend on Request, like non-persistent database\nconnection, consider shorthand method `addContext`. For static values consider reusing a `const` across your files.\n\n```ts\nimport { readFile } from \"node:fs/promises\";\nimport { defaultEndpointsFactory } from \"express-zod-api\";\n\nconst endpointsFactory = defaultEndpointsFactory.addContext(async () =\u003e {\n  // caution: new connection on every request:\n  const db = mongoose.connect(\"mongodb://connection.string\");\n  const privateKey = await readFile(\"private-key.pem\", \"utf-8\");\n  return { db, privateKey };\n});\n```\n\n**Notice on resources cleanup**: If necessary, you can release resources at the end of the request processing in a\ncustom [Result Handler](#response-customization):\n\n```ts\nimport { ResultHandler } from \"express-zod-api\";\n\nconst resultHandlerWithCleanup = new ResultHandler({\n  handler: ({ ctx }) =\u003e {\n    // necessary to check the presence of a certain property:\n    if (\"db\" in ctx \u0026\u0026 ctx.db) {\n      ctx.db.connection.close(); // sample cleanup\n    }\n  },\n});\n```\n\n## Using native express middlewares\n\nThere are two ways of connecting the native express middlewares depending on their nature and your objective.\n\nIn case it's a middleware establishing and serving its own routes, or somehow globally modifying the behaviour, or\nbeing an additional request parser (like `cookie-parser`), use the `beforeRouting` option. However, it might be better\nto avoid `cors` here — [the framework handles it on its own](#cross-origin-resource-sharing).\n\n```ts\nimport { createConfig } from \"express-zod-api\";\nimport ui from \"swagger-ui-express\";\n\nconst config = createConfig({\n  beforeRouting: ({ app, getLogger }) =\u003e {\n    const logger = getLogger();\n    logger.info(\"Serving the API documentation at https://example.com/docs\");\n    app.use(\"/docs\", ui.serve, ui.setup(documentation));\n    app.use(\"/custom\", (req, res, next) =\u003e {\n      const childLogger = getLogger(req); // if childLoggerProvider is configured\n    });\n  },\n});\n```\n\nIn case you need a special processing of `request`, or to modify the `response` for selected endpoints, use the method\n`addExpressMiddleware()` of `EndpointsFactory` (or its alias `use()`). The method has two optional features: a provider\nof a [context](#context) and an error transformer for adjusting the response status code.\n\n```ts\nimport { defaultEndpointsFactory } from \"express-zod-api\";\nimport createHttpError from \"http-errors\";\nimport { auth } from \"express-oauth2-jwt-bearer\";\n\nconst factory = defaultEndpointsFactory.use(auth(), {\n  provider: (req) =\u003e ({ auth: req.auth }), // optional, can be async\n  transformer: (err) =\u003e createHttpError(401, err.message), // optional\n});\n```\n\n## Refinements\n\nYou can implement additional validations within schemas using refinements.\nValidation errors are reported in a response with a status code `400`.\n\n```ts\nimport { z } from \"zod\";\nimport { Middleware } from \"express-zod-api\";\n\nconst nicknameConstraintMiddleware = new Middleware({\n  input: z.object({\n    nickname: z\n      .string()\n      .min(1)\n      .refine(\n        (nick) =\u003e !/^\\d.*$/.test(nick),\n        \"Nickname cannot start with a digit\",\n      ),\n  }),\n  // ...,\n});\n```\n\nBy the way, you can also refine the whole I/O object, for example in case you need a complex validation of its props.\n\n```ts\nconst endpoint = endpointsFactory.build({\n  input: z\n    .object({\n      email: z.email().optional(),\n      id: z.string().optional(),\n      otherThing: z.string().optional(),\n    })\n    .refine(\n      (inputs) =\u003e Object.keys(inputs).length \u003e= 1,\n      \"Please provide at least one property\",\n    ),\n  // ...,\n});\n```\n\n## Query string parser\n\nIn Express 5 the default query string parser was changed from \"extended\" (which is the `qs` module) to \"simple\" (which\nis the `node:querystring` module). The \"extended\" parser supports nested objects and arrays with optional indexes in\nsquare brackets. You can choose between those parsers as well as configure a custom implementation:\n\n| `queryParser` value                    | Query string example for arrays                  |\n| -------------------------------------- | ------------------------------------------------ |\n| simple                                 | `?values=1\u0026values=2\u0026values=3`                    |\n| extended                               | as simple or `?values[]=1\u0026values[]=2\u0026values[]=3` |\n| `(str) =\u003e qs.parse(str, {comma:true})` | as extended or `?values=1,2,3`                   |\n\n## Transformations\n\nSince parameters of GET requests come in the form of strings, there is often a need to transform them into numbers.\n\n```ts\nimport { z } from \"zod\";\n\nconst getUserEndpoint = endpointsFactory.buildVoid({\n  input: z.object({\n    id: z.string().transform((id) =\u003e parseInt(id, 10)),\n  }),\n  handler: async ({ input: { id }, logger }) =\u003e {\n    logger.debug(\"id\", typeof id); // number\n  },\n});\n```\n\n## Top level transformations and mapping\n\nFor some APIs it may be important that public interfaces such as query parameters use snake case, while the\nimplementation itself requires camel case for internal naming. In order to facilitate interoperability between the\ndifferent naming standards you can `.transform()` the entire `input` schema into another object using a well-typed\nmapping library, such as [camelize-ts](https://www.npmjs.com/package/camelize-ts). However, that approach would not be\nenough for the `output` schema if you're also aiming to [generate a valid documentation](#creating-a-documentation),\nbecause the transformations themselves do not contain schemas. Addressing this case, the framework offers the `.remap()`\nmethod of the object schema, a part of the [Zod plugin](#zod-plugin), which under the hood, in addition to the\ntransformation, also `.pipe()` the transformed object into a new object schema.\nHere is a recommended solution: it is important to use shallow transformations only.\n\n```ts\nimport camelize from \"camelize-ts\";\nimport snakify from \"snakify-ts\";\nimport { z } from \"zod\";\n\nconst endpoint = endpointsFactory.build({\n  input: z\n    .object({ user_id: z.string() })\n    .transform((inputs) =\u003e camelize(inputs, /* shallow: */ true)),\n  output: z\n    .object({ userName: z.string() })\n    .remap((outputs) =\u003e snakify(outputs, /* shallow: */ true)),\n  handler: async ({ input: { userId }, logger }) =\u003e {\n    logger.debug(\"user_id became userId\", userId);\n    return { userName: \"Agneta\" }; // becomes \"user_name\" in response\n  },\n});\n```\n\nThe `.remap()` method can also accept an object with an explicitly defined naming of your choice. The original keys\nmissing in that object remain unchanged (partial mapping).\n\n```ts\nz.object({ user_name: z.string(), id: z.number() }).remap({\n  user_name: \"weHAVEreallyWEIRDnamingSTANDARDS\", // \"id\" remains intact\n});\n```\n\n## Dealing with dates\n\nDates in JavaScript are one of the most troublesome entities. In addition, `Date` cannot be passed directly in JSON\nformat. Therefore, attempting to return `Date` from the endpoint handler results in it being converted to an ISO string\nin actual response by calling\n[toJSON()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON),\nwhich in turn calls\n[toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).\nIt is also impossible to transmit the `Date` in its original form to your endpoints within JSON. Therefore, there is\nconfusion with original method ~~z.date()~~ that is not recommended to use without transformations.\n\nIn order to solve this problem, the framework provides two custom methods for dealing with dates: `ez.dateIn()` and\n`ez.dateOut()` for using within input and output schemas accordingly.\n\n`ez.dateIn()` is a transforming schema that accepts an ISO `string` representation of a `Date`, validates it, and\nprovides your endpoint handler or middleware with a `Date`. It supports the following formats:\n\n```text\n2021-12-31T23:59:59.000Z\n2021-12-31T23:59:59Z\n2021-12-31T23:59:59\n2021-12-31\n```\n\n`ez.dateOut()`, on the contrary, accepts a `Date` and provides `ResultHandler` with a `string` representation in ISO\nformat for the response transmission. Both schemas accept metadata as an argument. Consider the following example:\n\n```ts\nimport { z } from \"zod\";\nimport { ez, defaultEndpointsFactory } from \"express-zod-api\";\n\nconst updateUserEndpoint = defaultEndpointsFactory.build({\n  method: \"post\",\n  input: z.object({\n    userId: z.string(),\n    birthday: ez.dateIn({ examples: [\"1963-04-21\"] }), // string -\u003e Date in handler\n  }),\n  output: z.object({\n    createdAt: ez.dateOut({ examples: [\"2021-12-31\"] }), // Date -\u003e string in response\n  }),\n  handler: async ({ input }) =\u003e ({\n    createdAt: new Date(\"2022-01-22\"), // 2022-01-22T00:00:00.000Z\n  }),\n});\n```\n\n## Cross-Origin Resource Sharing\n\nYou can enable your API for other domains using the corresponding configuration option `cors`. The value is required to\nensure you explicitly choose the correct setting. In addition to being a boolean, `cors` can also be assigned a\nfunction that overrides default CORS headers. That function has several parameters and can be asynchronous.\n\n```ts\nimport { createConfig } from \"express-zod-api\";\n\nconst config = createConfig({\n  /** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS */\n  cors: ({ defaultHeaders, request, endpoint, logger }) =\u003e ({\n    ...defaultHeaders,\n    \"Access-Control-Max-Age\": \"5000\",\n  }),\n});\n```\n\nPlease note: If you only want to send specific headers on requests to a specific endpoint, consider the\n[Middlewares](#middlewares) or [response customization approach](#response-customization).\n\n## Enabling HTTPS\n\nThe modern API standard often assumes the use of a secure data transfer protocol, confirmed by a TLS certificate, also\noften called an SSL certificate in habit. This way you can additionally (or solely) configure and run the HTTPS server:\n\n```ts\nimport { createConfig, createServer } from \"express-zod-api\";\n\nconst config = createConfig({\n  https: {\n    options: {\n      cert: fs.readFileSync(\"fullchain.pem\", \"utf-8\"),\n      key: fs.readFileSync(\"privkey.pem\", \"utf-8\"),\n    },\n    listen: 443, // port, UNIX socket or options\n  }, // ... cors, logger, etc\n});\n\n// 'await' is only needed if you're going to use the returned entities.\n// For top level CJS you can wrap you code with (async () =\u003e { ... })()\nconst { app, servers, logger } = await createServer(config, routing);\n```\n\nEnsure having `@types/node` package installed. At least you need to specify the port (usually it is 443) or UNIX socket,\ncertificate and the key, issued by the certifying authority. For example, you can acquire a free TLS certificate for\nyour API at [Let's Encrypt](https://letsencrypt.org/).\n\n## Enabling compression\n\nAccording to [Express.js best practices guide](https://expressjs.com/en/advanced/best-practice-performance.html)\nit might be a good idea to enable GZIP and Brotli compression for your API responses.\n\nInstall `compression` and `@types/compression`, and enable or configure compression:\n\n```ts\nimport { createConfig } from \"express-zod-api\";\n\nconst config = createConfig({\n  /** @link https://www.npmjs.com/package/compression#options */\n  compression: { threshold: \"1kb\" }, // or true\n});\n```\n\nIn order to receive a compressed response the client should include the following header in the request:\n`Accept-Encoding: br, gzip, deflate`. Only responses with compressible content types are subject to compression.\n\n## Customizing logger\n\nA simple built-in console logger is used by default with the following options that you can configure:\n\n```ts\nimport { createConfig } from \"express-zod-api\";\nconst config = createConfig({\n  logger: {\n    level: \"debug\", // or \"warn\" in production mode\n    color: undefined, // detects automatically, boolean\n    depth: 2, // controls how deeply entities should be inspected\n  },\n});\n```\n\nYou can also replace it with a one having at least the following methods: `info()`, `debug()`, `error()` and `warn()`.\nWinston and Pino support is well known. Here is an example configuring `pino` logger with `pino-pretty` extension:\n\n```ts\nimport pino, { Logger } from \"pino\";\nimport { createConfig } from \"express-zod-api\";\n\nconst logger = pino({\n  transport: {\n    target: \"pino-pretty\",\n    options: { colorize: true },\n  },\n});\nconst config = createConfig({ logger });\n\n// Setting the type of logger used\ndeclare module \"express-zod-api\" {\n  interface LoggerOverrides extends Logger {}\n}\n```\n\n## Child logger\n\nIn case you need a dedicated logger for each request (for example, equipped with a request ID), you can specify the\n`childLoggerProvider` option in your configuration. The function accepts the initially defined logger and the request,\nit can also be asynchronous. The child logger returned by that function will replace the `logger` in all handlers.\nYou can use the `.child()` method of the built-in logger or [install a custom logger](#customizing-logger) instead.\n\n```ts\nimport { createConfig, BuiltinLogger } from \"express-zod-api\";\nimport { randomUUID } from \"node:crypto\";\n\n// This enables the .child() method on \"logger\":\ndeclare module \"express-zod-api\" {\n  interface LoggerOverrides extends BuiltinLogger {}\n}\n\nconst config = createConfig({\n  childLoggerProvider: ({ parent, request }) =\u003e\n    parent.child({ requestId: randomUUID() }), // accessible at logger.ctx.requestId later\n});\n```\n\n# Advanced features\n\n## Customizing input sources\n\nYou can customize the list of `request` properties that are combined into `input` that is being validated and available\nto your endpoints and middlewares. The order here matters: each next item in the array has a higher priority than its\nprevious sibling. The following arrangement is default:\n\n```ts\nimport { createConfig } from \"express-zod-api\";\n\ncreateConfig({\n  inputSources: {\n    get: [\"query\", \"params\"],\n    post: [\"body\", \"params\", \"files\"],\n    put: [\"body\", \"params\"],\n    patch: [\"body\", \"params\"],\n    delete: [\"query\", \"params\"],\n  }, // ...\n});\n```\n\n## Headers as input source\n\nIn a similar way you can enable request headers as the input source. This is an opt-in feature. Please note:\n\n- consider giving `headers` the lowest priority among other `inputSources` to avoid overwrites;\n- consider handling headers in `Middleware` and declaring them within `security` property to improve `Documentation`;\n- the request headers acquired that way are always lowercase when describing their validation schemas.\n\n```ts\nimport { createConfig, Middleware } from \"express-zod-api\";\nimport { z } from \"zod\";\n\ncreateConfig({\n  inputSources: {\n    get: [\"headers\", \"query\"], // headers have lowest priority\n  }, // ...\n});\n\nnew Middleware({\n  security: { type: \"header\", name: \"token\" }, // recommended\n  input: z.object({ token: z.string() }),\n});\n\nfactory.build({\n  input: z.object({\n    \"x-request-id\": z.string(), // this one is from request.headers\n    id: z.string(), // this one is from request.query\n  }), // ...\n});\n```\n\n## Response customization\n\n`ResultHandler` is responsible for transmitting consistent responses containing the endpoint output or an error.\nThe `defaultResultHandler` sets the HTTP status code and ensures the following type of the response:\n\n```ts\ntype DefaultResponse\u003cOUT\u003e =\n  | { status: \"success\"; data: OUT } // Positive response\n  | { status: \"error\"; error: { message: string } }; // or Negative response\n```\n\nYou can create your own result handler by using this example as a template:\n\n```ts\nimport { z } from \"zod\";\nimport {\n  ResultHandler,\n  ensureHttpError,\n  getMessageFromError,\n} from \"express-zod-api\";\n\nconst yourResultHandler = new ResultHandler({\n  positive: (data) =\u003e ({\n    schema: z.object({ data }),\n    mimeType: \"application/json\", // optinal or array\n  }),\n  negative: z.object({ error: z.string() }),\n  handler: ({ error, input, output, request, response, logger }) =\u003e {\n    if (error) {\n      const { statusCode } = ensureHttpError(error);\n      const message = getMessageFromError(error);\n      return void response.status(statusCode).json({ error: message });\n    }\n    response.status(200).json({ data: output });\n  },\n});\n```\n\n_See also [Different responses for different status codes](#different-responses-for-different-status-codes)_.\n\nAfter creating your custom `ResultHandler` you can use it as an argument for `EndpointsFactory` instance creation:\n\n```ts\nimport { EndpointsFactory } from \"express-zod-api\";\n\nconst endpointsFactory = new EndpointsFactory(yourResultHandler);\n```\n\n## Empty response\n\nFor some REST APIs, empty responses are typical: with status code `204` (No Content) and redirects (302). In order to\ndescribe it set the `mimeType` to `null` and `schema` to `z.never()`:\n\n```ts\nconst resultHandler = new ResultHandler({\n  positive: { statusCode: 204, mimeType: null, schema: z.never() },\n  negative: { statusCode: 404, mimeType: null, schema: z.never() },\n});\n```\n\n## Non-JSON response\n\nTo configure a non-JSON responses (for example, to send an image file) you should specify its MIME type.\n\nYou can find two approaches to `EndpointsFactory` and `ResultHandler` implementation\n[in this example](https://github.com/RobinTail/express-zod-api/blob/master/example/factories.ts).\nOne of them implements file streaming, in this case the endpoint just has to provide the filename.\nThe response schema can be `z.string()`, `z.base64()` or `ez.buffer()` to reflect the data accordingly in the\n[generated documentation](#creating-a-documentation).\n\n```ts\nconst fileStreamingEndpointsFactory = new EndpointsFactory(\n  new ResultHandler({\n    positive: { schema: ez.buffer(), mimeType: \"image/*\" },\n    negative: { schema: z.string(), mimeType: \"text/plain\" },\n    handler: ({ response, error, output }) =\u003e {\n      if (error) return void response.status(400).send(error.message);\n      if (\"filename\" in output)\n        fs.createReadStream(output.filename).pipe(\n          response.attachment(output.filename),\n        );\n      else response.status(400).send(\"Filename is missing\");\n    },\n  }),\n);\n```\n\n## Error handling\n\nAll runtime errors are handled by a `ResultHandler`. The default is `defaultResultHandler`. Using `ensureHttpError()`\nit normalizes errors into consistent HTTP responses with sensible status codes. Errors can originate from three layers:\n\n- `Endpoint` execution (including attached `Middleware`):\n  - Handled by a `ResultHandler` used by `EndpointsFactory` (`defaultEndpointsFactory` uses `defaultResultHandler`);\n  - `InputValidationError`: request violates `input` schema, the default status code is `400`;\n  - `OutputValidationError`: handler violates `output` schema, the default status code is `500`;\n  - `HttpError`: can be thrown in handlers with help of `createHttpError()`, its `.statusCode` is used for response;\n  - For other errors the default status code is `500`;\n- Routing, parsing and upload issues:\n  - Handled by `ResultHandler` configured as `errorHandler` (the defaults is `defaultResultHandler`);\n  - Parsing errors: passed through as-is (typically `HttpError` with `4XX` code used for response by default);\n  - Routing errors: `404` or `405`, based on `wrongMethodBehavior` configuration;\n  - Upload issues: thrown only if `upload.limitError` is configured (`HttpError::statusCode` can be used for response);\n  - For other errors the default status code is `500`;\n- `ResultHandler` failures:\n  - Handled by `LastResortHandler` with status code `500` and a plain text response.\n\nYou can customize it by passing a custom `ResultHandler` to `EndpointsFactory` and by configuring `errorHandler`.\n\n## Production mode\n\nConsider enabling production mode by setting `NODE_ENV` environment variable to `production` for your deployment:\n\n- Express activates some [performance optimizations](https://expressjs.com/en/advanced/best-practice-performance.html);\n- Self-diagnosis for potential problems is disabled to ensure faster startup;\n- The `defaultResultHandler`, `defaultEndpointsFactory` and `LastResortHandler` generalize server-side error messages\n  in negative responses in order to improve the security of your API by not disclosing the exact causes of errors:\n  - Throwing errors that have or imply `5XX` status codes become just `Internal Server Error` message in response;\n  - You can control that behavior by throwing errors using `createHttpError()` and using its `expose` option:\n\n```ts\nimport createHttpError from \"http-errors\";\n// NODE_ENV=production\n// Throwing HttpError from Endpoint or Middleware that is using defaultResultHandler or defaultEndpointsFactory:\ncreateHttpError(401, \"Token expired\"); // —\u003e \"Token expired\"\ncreateHttpError(401, \"Token expired\", { expose: false }); // —\u003e \"Unauthorized\"\ncreateHttpError(500, \"Something is broken\"); // —\u003e \"Internal Server Error\"\ncreateHttpError(501, \"We didn't make it yet\", { expose: true }); // —\u003e \"We didn't make it yet\"\n```\n\n## HTML Forms (URL encoded)\n\nUse the proprietary schema `ez.form()` with an object shape or a custom `z.object()` with form fields in order to\ndescribe the `input` schema of an Endpoint. Requests to the Endpoint are parsed using the `formParser` config option,\nwhich is `express.urlencoded()` by default. The request content type should be `application/x-www-form-urlencoded`\n(default for HTML forms without uploads).\n\n```ts\nimport { defaultEndpointsFactory, ez } from \"express-zod-api\";\nimport { z } from \"zod\";\n\nexport const submitFeedbackEndpoint = defaultEndpointsFactory.build({\n  method: \"post\",\n  input: ez.form({\n    name: z.string().min(1),\n    email: z.email(),\n    message: z.string().min(1),\n  }),\n});\n```\n\n_Hint: for unlisted extra fields use the following syntax: `ez.form( z.object({}).passthrough() )`._\n\n## File uploads\n\nInstall the following additional packages: `express-fileupload` and `@types/express-fileupload`, and enable or\nconfigure file uploads. Refer to [documentation](https://www.npmjs.com/package/express-fileupload#available-options) on\navailable options. The `limitHandler` option is replaced by the `limitError` one. You can also connect an additional\nmiddleware for restricting the ability to upload using the `beforeUpload` option. So the configuration for the limited\nand restricted upload might look this way:\n\n```ts\nimport createHttpError from \"http-errors\";\n\nconst config = createConfig({\n  upload: /* true or options: */ {\n    limits: { fileSize: 51200 }, // 50 KB\n    limitError: createHttpError(413, \"The file is too large\"), // handled by errorHandler in config\n    beforeUpload: ({ request, logger }) =\u003e {\n      if (!canUpload(request)) throw createHttpError(403, \"Not authorized\");\n    },\n    debug: true, // default\n  },\n});\n```\n\nThen use `ez.upload()` schema for a corresponding property. The request content type must be `multipart/form-data`:\n\n```ts\nimport { z } from \"zod\";\nimport { ez, defaultEndpointsFactory } from \"express-zod-api\";\n\nconst fileUploadEndpoint = defaultEndpointsFactory.build({\n  method: \"post\",\n  input: z.object({\n    avatar: ez.upload(), // \u003c--\n  }),\n  output: z.object({}),\n  handler: async ({ input: { avatar } }) =\u003e {\n    // avatar: {name, mv(), mimetype, data, size, etc}\n    // avatar.truncated is true on failure when limitError option is not set\n  },\n});\n```\n\n_You can still send other data and specify additional `input` parameters, including arrays and objects._\n\n## Connect to your own express app\n\nIf you already have your own configured express application, or you find the framework settings not enough, you can\nconnect the endpoints to your app or any express router using the `attachRouting()` method:\n\n```ts\nimport express from \"express\";\nimport { createConfig, attachRouting, Routing } from \"express-zod-api\";\n\nconst app = express(); // or express.Router()\nconst config = createConfig({ app /* cors, logger, ... */ });\nconst routing: Routing = {}; // your endpoints go here\n\nconst { notFoundHandler, logger } = attachRouting(config, routing);\n\napp.use(notFoundHandler); // optional\napp.listen();\nlogger.info(\"Glory to science!\");\n```\n\n**Please note** that in this case you probably need to parse `request.body`, call `app.listen()` and handle `404`\nerrors yourself. In this regard `attachRouting()` provides you with `notFoundHandler` which you can optionally connect\nto your custom express app.\n\nBesides that, if you're looking to include additional request parsers, or a middleware that establishes its own routes,\nthen consider using the `beforeRouting` [option in config instead](#using-native-express-middlewares).\n\n## Testing endpoints\n\nThe way to test endpoints is to mock the request, response, and logger objects, invoke the `execute()` method, and\nassert the expectations on status, headers and payload. The framework provides a special method `testEndpoint` that\nmakes mocking easier. Under the hood, request and response object are mocked using the\n[node-mocks-http](https://www.npmjs.com/package/node-mocks-http) library, therefore you can utilize its API for\nsettings additional properties and asserting expectation using the provided getters, such as `._getStatusCode()`.\n\n```ts\nimport { testEndpoint } from \"express-zod-api\";\n\ntest(\"should respond successfully\", async () =\u003e {\n  const { responseMock, loggerMock } = await testEndpoint({\n    endpoint: yourEndpoint,\n    requestProps: {\n      method: \"POST\", // default: GET\n      body: {}, // incoming data as if after parsing (JSON)\n    }, // responseOptions, configProps, loggerProps\n  });\n  expect(loggerMock._getLogs().error).toHaveLength(0);\n  expect(responseMock._getStatusCode()).toBe(200);\n  expect(responseMock._getHeaders()).toHaveProperty(\"x-custom\", \"one\"); // lower case!\n  expect(responseMock._getJSONData()).toEqual({ status: \"success\" });\n});\n```\n\n## Testing middlewares\n\nMiddlewares can also be tested individually using the `testMiddleware()` method. You can also pass `ctx` collected\nfrom returns of previous middlewares, if the one being tested somehow depends on it. Possible errors would be handled\neither by `errorHandler` configured within given `configProps` or `defaultResultHandler`.\n\n```ts\nimport { z } from \"zod\";\nimport { Middleware, testMiddleware } from \"express-zod-api\";\n\nconst middleware = new Middleware({\n  input: z.object({ test: z.string() }),\n  handler: async ({ ctx, input: { test } }) =\u003e ({\n    collectedContext: Object.keys(ctx),\n    testLength: test.length,\n  }),\n});\n\nconst { output, responseMock, loggerMock } = await testMiddleware({\n  middleware,\n  requestProps: { method: \"POST\", body: { test: \"something\" } },\n  ctx: { prev: \"accumulated\" }, // responseOptions, configProps, loggerProps\n});\nexpect(loggerMock._getLogs().error).toHaveLength(0);\nexpect(output).toEqual({ collectedContext: [\"prev\"], testLength: 9 });\n```\n\n# Integration and Documentation\n\n## Zod Plugin\n\nExpress Zod API augments Zod using [Zod Plugin](https://www.npmjs.com/package/@express-zod-api/zod-plugin),\nadding the runtime helpers the framework relies on.\n\n## Generating a Frontend Client\n\nYou can generate a TypeScript file containing the IO types of your API and a client for it. Make sure you have\n`typescript` installed. Consider also installing `prettier` and using the async `printFormatted()` method.\n\n```ts\nimport typescript from \"typescript\";\nimport { Integration } from \"express-zod-api\";\n\nconst client = new Integration({\n  typescript, // or await Integration.create() to delegate importing\n  routing,\n  config,\n  variant: \"client\", // \u003c— optional, see also \"types\" for a DIY solution\n});\n\nconst prettierFormattedTypescriptCode = await client.printFormatted(); // or just .print() for unformatted\n```\n\nAlternatively, you can supply your own `format` function into that method or use a regular `print()` method instead.\nThe generated client is flexibly configurable on the frontend side for using a custom implementation function that\nmakes requests using the libraries and methods of your choice. The default implementation uses `fetch`. The client\nasserts the type of request parameters and response. Consuming the generated client requires TypeScript version 4.1+.\n\n```ts\nimport { Client, Implementation, Subscription } from \"./client.ts\"; // the generated file\n\nconst client = new Client(/* optional custom Implementation */);\nclient.provide(\"get /v1/user/retrieve\", { id: \"10\" });\nclient.provide(\"post /v1/user/:id\", { id: \"10\" }); // it also substitutes path params\nnew Subscription(\"get /v1/events/stream\", {}).on(\"time\", (time) =\u003e {}); // Server-sent events (SSE)\n```\n\n## Creating a documentation\n\nYou can generate the specification of your API and write it to a `.yaml` file, that can be used as the documentation:\n\n```ts\nimport { Documentation } from \"express-zod-api\";\n\nconst yamlString = new Documentation({\n  routing, // the same routing and config that you use to start the server\n  config,\n  version: \"1.2.3\",\n  title: \"Example API\",\n  serverUrl: \"https://example.com\",\n  composition: \"inline\", // optional, or \"components\" for keeping schemas in a separate dedicated section using refs\n  // descriptions: { positiveResponse, negativeResponse, requestParameter, requestBody }, // check out these features\n}).getSpecAsYaml();\n```\n\nYou can add descriptions and examples to your endpoints, their I/O schemas and their properties. It will be included\ninto the generated documentation of your API. Consider the following example:\n\n```ts\nimport { defaultEndpointsFactory } from \"express-zod-api\";\n\nconst exampleEndpoint = defaultEndpointsFactory.build({\n  shortDescription: \"Retrieves the user.\", // \u003c—— this becomes the summary line\n  description: \"The detailed explanaition on what this endpoint does.\",\n  input: z.object({\n    id: z\n      .string()\n      .example(\"123\") // input examples should be set before transformations\n      .transform(Number)\n      .describe(\"the ID of the user\"),\n  }),\n  // ..., similarly for output and middlewares\n});\n```\n\nYou can also use `schema.meta({ id: \"UniqueName\" })` for custom schema naming.\n_See the complete example of the generated documentation\n[here](https://github.com/RobinTail/express-zod-api/blob/master/example/example.documentation.yaml)_\n\n## Tagging the endpoints\n\nWhen generating documentation, you may find it necessary to classify endpoints into groups. The possibility of tagging\nendpoints is available for that purpose. In order to establish the constraints on tags across all the endpoints, they\nshould be declared as keys of `TagOverrides` interface. Consider the following example:\n\n```ts\nimport { defaultEndpointsFactory, Documentation } from \"express-zod-api\";\n\n// Add similar declaration once, somewhere in your code, preferably near config\ndeclare module \"express-zod-api\" {\n  interface TagOverrides {\n    users: unknown;\n    files: unknown;\n    subscriptions: unknown;\n  }\n}\n\n// Use the declared tags for endpoints\nconst exampleEndpoint = defaultEndpointsFactory.build({\n  tag: \"users\", // or array [\"users\", \"files\"]\n});\n\n// Add extended description of the tags to Documentation (optional)\nnew Documentation({\n  tags: {\n    users: \"All about users\",\n    files: { description: \"All about files\", url: \"https://example.com\" },\n  },\n});\n```\n\n## Deprecated schemas and routes\n\nAs your API evolves, you may need to mark some parameters or routes as deprecated before deleting them. For this\npurpose, the `.deprecated()` method is available on each schema and `Endpoint`, it's immutable.\nYou can also deprecate all routes the `Endpoint` assigned to by setting `EndpointsFactory::build({ deprecated: true })`.\n\n```ts\nimport { Routing } from \"express-zod-api\";\nimport { z } from \"zod\";\n\nconst someEndpoint = factory.build({\n  deprecated: true, // deprecates all routes the endpoint assigned to\n  input: z.object({\n    prop: z.string().deprecated(), // deprecates the property or a path parameter\n  }),\n});\n\nconst routing: Routing = {\n  v1: oldEndpoint.deprecated(), // deprecates the /v1 path\n  v2: someEndpoint, // the path is assigned with initially deprecated endpoint (also deprecated)\n};\n```\n\n## Customizable brands handling\n\nYou can customize handling rules for your schemas in Documentation and Integration. Use the `.brand()` method on your\nschema to make it special and distinguishable for the framework in runtime. Using symbols is recommended for branding.\nAfter that utilize the `brandHandling` feature of both constructors to declare your custom implementation. In case you\nneed to reuse a handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.\n\n```ts\nimport ts from \"typescript\";\nimport { z } from \"zod\";\nimport {\n  Documentation,\n  Integration,\n  Depicter,\n  Producer,\n} from \"express-zod-api\";\n\nconst myBrand = Symbol(\"MamaToldMeImSpecial\"); // I recommend to use symbols for this purpose\nconst myBrandedSchema = z.string().brand(myBrand);\n\nconst ruleForDocs: Depicter = (\n  { zodSchema, jsonSchema }, // jsonSchema is the default depiction\n  { path, method, isResponse },\n) =\u003e ({\n  ...jsonSchema,\n  summary: \"Special type of data\",\n});\n\nconst ruleForClient: Producer = (\n  schema: typeof myBrandedSchema, // you should assign type yourself\n  { next, isResponse }, // handle a nested schema using next()\n) =\u003e ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);\n\nnew Documentation({\n  brandHandling: { [myBrand]: ruleForDocs },\n});\n\nnew Integration({\n  brandHandling: { [myBrand]: ruleForClient },\n});\n```\n\n# Special needs\n\n## Different responses for different status codes\n\nIn some special cases you may want the ResultHandler to respond slightly differently depending on the status code,\nfor example if your API strictly follows REST standards. It may also be necessary to reflect this difference in the\ngenerated Documentation. For that purpose, the constructor of `ResultHandler` accepts flexible declaration of possible\nresponse schemas and their corresponding status codes.\n\n```ts\nimport { ResultHandler } from \"express-zod-api\";\n\nnew ResultHandler({\n  positive: (data) =\u003e ({\n    statusCode: [201, 202], // created or will be created\n    schema: z.object({ status: z.literal(\"created\"), data }),\n  }),\n  negative: [\n    {\n      statusCode: 409, // conflict: entity already exists\n      schema: z.object({ status: z.literal(\"exists\"), id: z.int() }),\n    },\n    {\n      statusCode: [400, 500], // validation or internal error\n      schema: z.object({ status: z.literal(\"error\"), reason: z.string() }),\n    },\n  ],\n  handler: ({ error, response, output }) =\u003e {\n    // your implementation here\n  },\n});\n```\n\n## Array response\n\nPlease avoid doing this in new projects: responding with array is a bad practice keeping your endpoints from evolving\nin backward compatible way (without making breaking changes). Nevertheless, for the purpose of easier migration of\nlegacy APIs to this framework consider using `arrayResultHandler` or `arrayEndpointsFactory` instead of default ones,\nor implement your own ones in a similar way.\nThe `arrayResultHandler` expects your endpoint to have `items` property in the `output` object schema. The array\nassigned to that property is used as the response. This approach also supports examples, as well as documentation and\nclient generation. Check out [the example endpoint](/example/endpoints/list-users.ts) for more details.\n\n## Accepting raw data\n\nSome APIs may require an endpoint to be able to accept and process raw data, such as streaming or uploading a binary\nfile as an entire body of request. Use the proprietary `ez.raw()` schema as the input schema of your endpoint.\nThe default parser in this case is `express.raw()`. You can customize it by assigning the `rawParser` option in config.\nThe raw data is placed into `request.body.raw` property, having type `Buffer`.\n\n```ts\nimport { defaultEndpointsFactory, ez } from \"express-zod-api\";\n\nconst rawAcceptingEndpoint = defaultEndpointsFactory.build({\n  method: \"post\",\n  input: ez.raw({\n    /* the place for additional inputs, like route params, if needed */\n  }),\n  output: z.object({ length: z.int().nonnegative() }),\n  handler: async ({ input: { raw } }) =\u003e ({\n    length: raw.length, // raw is Buffer\n  }),\n});\n```\n\n## Profiling\n\nFor debugging and performance testing purposes the framework offers a simple `.profile()` method on the built-in logger.\nIt starts a timer when you call it and measures the duration in adaptive units (from picoseconds to minutes) until you\ninvoke the returned callback. The default severity of those measurements is `debug`.\n\n```ts\nimport { createConfig, BuiltinLogger } from \"express-zod-api\";\n\n// This enables the .profile() method on built-in logger:\ndeclare module \"express-zod-api\" {\n  interface LoggerOverrides extends BuiltinLogger {}\n}\n\n// Inside a handler of Endpoint, Middleware or ResultHandler:\nconst done = logger.profile(\"expensive operation\");\ndoExpensiveOperation();\ndone(); // debug: expensive operation '555 milliseconds'\n```\n\nYou can also customize the profiler with your own formatter, chosen severity or even performance assessment function:\n\n```ts\nlogger.profile({\n  message: \"expensive operation\",\n  severity: (ms) =\u003e (ms \u003e 500 ? \"error\" : \"info\"), // assess immediately\n  formatter: (ms) =\u003e `${ms.toFixed(2)}ms`, // custom format\n});\ndoExpensiveOperation();\ndone(); // error: expensive operation '555.55ms'\n```\n\n## Graceful shutdown\n\nYou can enable and configure a special request monitoring that, if it receives a signal to terminate a process, will\nfirst put the server into a mode that rejects new requests, attempt to complete started requests within the specified\ntime, and then forcefully stop the server and terminate the process.\n\n```ts\nimport { createConfig } from \"express-zod-api\";\n\ncreateConfig({\n  gracefulShutdown: {\n    timeout: 1000,\n    events: [\"SIGINT\", \"SIGTERM\"],\n    beforeExit: /* async */ () =\u003e {},\n  },\n});\n```\n\n## Subscriptions\n\nIf you want the user of a client application to be able to subscribe to subsequent updates initiated by the server,\nconsider [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) feature.\nClient application can subscribe to the event stream using `EventSource` class instance or the\n[instance of the generated](#generating-a-frontend-client) `Subscription` class. The following example demonstrates\nthe implementation emitting the `time` event each second.\n\n```ts\nimport { z } from \"zod\";\nimport { EventStreamFactory } from \"express-zod-api\";\nimport { setTimeout } from \"node:timers/promises\";\n\nconst subscriptionEndpoint = new EventStreamFactory({\n  time: z.int().positive(),\n}).buildVoid({\n  input: z.object({}), // optional input schema\n  handler: async ({ ctx: { emit, isClosed, signal } }) =\u003e {\n    while (!isClosed()) {\n      emit(\"time\", Date.now());\n      await setTimeout(1000);\n    }\n  },\n});\n```\n\nIf you need more capabilities, such as bidirectional event sending, I have developed an additional websocket operating\nframework, [Zod Sockets](https://github.com/RobinTail/zod-sockets), which has similar principles and capabilities.\n\n# Caveats\n\nThere are some well-known issues and limitations, or third party bugs that cannot be fixed in the usual way, but you\nshould be aware of them.\n\n## Excessive properties in endpoint output\n\nThe schema validator removes excessive properties by default. However, TypeScript\n[does not yet display errors](https://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks)\nin this case during development. You can achieve this verification by assigning the output schema to a constant and\nreusing it in forced type of the output:\n\n```ts\nconst output = z.object({ anything: z.number() });\n\nfactory.build({\n  output,\n  handler: async (): Promise\u003cz.input\u003ctypeof output\u003e\u003e =\u003e ({\n    anything: 123,\n    excessive: \"something\", // error TS2322, ok!\n  }),\n});\n```\n\n# Your input to my output\n\nIf you have a question or idea, or you found a bug, or vulnerability, or security issue, or want to make a PR:\nplease refer to [Contributing Guidelines](CONTRIBUTING.md).\n","funding_links":["https://github.com/sponsors/RobinTail","https://buymeacoffee.com/robintail"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobintail%2Fexpress-zod-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobintail%2Fexpress-zod-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobintail%2Fexpress-zod-api/lists"}