{"id":21478985,"url":"https://github.com/lstkz/defensive","last_synced_at":"2026-06-14T16:33:04.925Z","repository":{"id":46930876,"uuid":"155971754","full_name":"lstkz/defensive","owner":"lstkz","description":"Contract utility for TypeScript 🤖","archived":false,"fork":false,"pushed_at":"2021-09-21T07:56:39.000Z","size":15205,"stargazers_count":2,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-30T04:02:47.071Z","etag":null,"topics":["contract","logging","typescript","validation"],"latest_commit_sha":null,"homepage":"","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/lstkz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-03T10:17:11.000Z","updated_at":"2020-12-11T13:16:40.000Z","dependencies_parsed_at":"2022-09-23T09:12:27.604Z","dependency_job_id":null,"html_url":"https://github.com/lstkz/defensive","commit_stats":null,"previous_names":["bettercallsky/defensive"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lstkz/defensive","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lstkz%2Fdefensive","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lstkz%2Fdefensive/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lstkz%2Fdefensive/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lstkz%2Fdefensive/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lstkz","download_url":"https://codeload.github.com/lstkz/defensive/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lstkz%2Fdefensive/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34327691,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["contract","logging","typescript","validation"],"created_at":"2024-11-23T11:20:33.029Z","updated_at":"2026-06-14T16:33:04.906Z","avatar_url":"https://github.com/lstkz.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# defensive\n\n\n`defensive` is a TypeScript library for creating contracts (aka services) with a proper validation and logging.  \nIt depends on [veni](https://github.com/BetterCallSky/veni) (validator).  \nOnly node v10 and v12 are supported.\n\n\n[![Travis](https://img.shields.io/travis/BetterCallSky/defensive.svg)](https://travis-ci.org/BetterCallSky/defensive)\n[![codecov](https://codecov.io/gh/BetterCallSky/defensive/branch/master/graph/badge.svg)](https://codecov.io/gh/BetterCallSky/defensive)\n\n\n### About\nThe motivation is to provide a library for [contract programming](https://en.wikipedia.org/wiki/Design_by_contract) that works well with TypeScript.  \nThere are many existing libraries for data validation that rely heavily on decorator annotations. Unfortunately, decorators have many flaws:\n- it's an experimental feature, and its syntax is going to change,\n- redundant syntax because we must create special classes instead of using plain objects,\n- it's a runtime feature, and there are some [bugs related to reflection](https://github.com/kulshekhar/ts-jest/issues/439),\n- no type inference, any typos or mistakes cause a runtime error instead of a compilation error.\n\n Since Typescript 2.8, it's possible to use [conditional types](https://github.com/Microsoft/TypeScript/pull/21496), that allow us to map one type to another. It's a powerful feature that can extract a Typescript interface from javascript objects (implemented by [veni](https://github.com/BetterCallSky/veni)).\n\n See the example below. There are no TypeScript annotations. It's pure JavaScript code, but we have type checking inferred from Veni.\n\n\n![Alt text](./.github/type-checking.gif)\n\n### Features\n\n- Full type inference for input parameters.\n- Input validation and normalization (example: string type `\"2\"` to number type `2`).\n- Input logging (input parameters):\n```\nENTER myService#methodName: {param1: 'foo', param2: 'bar'}\n```\n- Output logging:\n```\nEXIT myService#methodName: {result: 'foobar', anotherProp: 'bar'}\n```\n- Error logging with input parameters (see example below).\n- Bindings to 3rd party frameworks (see example below).\n- Context (aka continuation local storage) - passing data between function calls without using function arguments (see example below).\n\n### Getting Started\n\n```bash\nnpm install defensive\n```\n```bash\nyarn add defensive\n```\n\n## Example usage \n\n```ts\n// contract.ts\nexport const { createContract } = initialize();\n\n// services/CalcService.ts\nimport { V } from 'veni';\nimport { createContract } from './contract';\nimport util from 'util';\n\nexport const add = createContract('CalcService#add')\n  .params('a', 'b')\n  .schema({\n    a: V.number(),\n    b: V.number(),\n  })\n  .fn(async (a, b) =\u003e a + b);\n\n(async function main() {\n  try {\n    await add(1, 3); // returns 4\n    await add('5' as any, '6' as any); // returns 11, input parameters are converted to number types\n    await add('1' as any, { foo: 'bar' } as any); // throws an error\n    // NOTE: you shouldn't use casting `as any` in your code. It's used only for a demonstration purpose.\n    // The service is expected to be called with unknown input (for example: req.body).\n  } catch (e) {\n    console.error(util.inspect(e, { depth: null }));\n  }\n})();\n```\n```\n$ ts-node -T examples/example1.ts\nENTER CalcService#add: { a: 1, b: 3 }\nEXIT CalcService#add: 4\nENTER CalcService#add: { a: '5', b: '6' }\nEXIT CalcService#add: 11\nENTER CalcService#add: { a: '1', b: { foo: 'bar' } }\n{ Error: ContractError: Validation error: 'b' must be a number.\n    at wrappedFunction (/defensive/src/_createContract.ts:81:17)\n    at process._tickCallback (internal/process/next_tick.js:68:7)\n    at Function.Module.runMain (internal/modules/cjs/loader.js:744:11)\n    at Object.\u003canonymous\u003e (/.nvm/versions/node/v10.12.0/lib/node_modules/ts-node/src/bin.ts:158:12)\n    at Module._compile (internal/modules/cjs/loader.js:688:30)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)\n    at Module.load (internal/modules/cjs/loader.js:598:32)\n    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)\n    at Function.Module._load (internal/modules/cjs/loader.js:529:3)\n    at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)\n  original:\n   { Error: Validation error: 'b' must be a number.\n       at new ValidationError (/defensive/node_modules/veni/ValidationError.js:19:28)\n       at Object.exports.validate (/defensive/node_modules/veni/validate.js:37:21)\n       at /defensive/src/wrapValidate.ts:17:24\n       at logDecorator (/defensive/src/wrapLog.ts:40:26)\n       at hook.runInNewScope (/defensive/src/_createContract.ts:67:52)\n       at AsyncResource.runInAsyncScope (async_hooks.js:188:21)\n       at ContractHook.runInNewScope (/defensive/src/ContractHook.ts:45:26)\n       at wrappedFunction (/defensive/src/_createContract.ts:67:32)\n       at main (/defensive/examples/example1.ts:24:11)\n       at process._tickCallback (internal/process/next_tick.js:68:7)\n     errors:\n      [ { type: 'number.base',\n          message: 'must be a number',\n          path: [ 'b' ],\n          value: { foo: 'bar' } } ] },\n  entries:\n   [ { signature: 'CalcService#add',\n       input: '{ a: \\'1\\', b: { foo: \\'bar\\' } }' } ] }\n```\n\nSee example under `examples/example1.ts`. Run it using `npm run example1`.\n\n\n## Removing security information\nBy default properties `password`, `token`, `accessToken` are removed from logging.  \nAdditionally you can set options to `{removeOutput: true}` to remove the method result.  \nExample:\n\nfile `services/SecurityService.ts`\n```ts\n// services/SecurityService.ts\nimport { createContract } from 'defensive';\nimport { V } from 'veni';\n \nconst hashPassword = createContract('SecurityService#hashPassword')\n  .params('password')\n  .schema({\n    password: V.string(),\n  })\n  .fn(async password =\u003e 'ba817ef716');\n\nhashPassword('secret-password');\n```\n```\n$ ts-node -T examples/example2.ts\nENTER SecurityService#hashPassword: { password: '\u003cremoved\u003e' }\nEXIT SecurityService#hashPassword: 'ba817ef716'\n```\n\nSee example under `examples/example2.ts`. Run it using `npm run example2`.\n\n### Notes\n- The wrapped function must have 0-4 arguments. \n- You can always override the inferred type. For example, if you to skip strict validation of properties.\n\n```ts\ncreateContract('CalcService#add')\n  .params('foo')\n  .schema({\n    foo: V.object(),\n  })\n  .fn((foo: SomeExistingObject) =\u003e {\n\n  });\n\n```\n\n## Creating bindings\nIt's possible to extend the contract prototype and add custom metadata that can be used to mount the contract in 3rd party frameworks or library.  \nFor example: you can create your own binding for an express app, graphql app, kafka events or cron jobs.  \n\nExample binding for Express  \n```ts\nimport { initialize, ContractBinding } from 'defensive';\nimport { V } from 'veni';\nimport { Request, Response, default as express, Handler } from 'express';\n\nconst { createContract } = initialize();\n\n// Creating binding definition\n// bindings.ts\n\nContractBinding.prototype.express = function(options) {\n  if (!this.fn.expressOptions) {\n    this.fn.expressOptions = [];\n  }\n  this.fn.expressOptions.push(options);\n  return this.fn as any;\n};\n\ninterface ExpressOptions {\n  auth?: boolean;\n  method: 'get' | 'post' | 'put' | 'delete' | 'patch';\n  path: string;\n  handler(req: Request, res: Response): Promise\u003cvoid\u003e;\n}\n\ndeclare module '../src' {\n  interface ContractBinding\u003cT\u003e {\n    expressOptions: ExpressOptions[];\n    express(options: ExpressOptions): T \u0026 ContractBinding\u003cT\u003e;\n  }\n}\n\n// Create service\n// UserService.ts\n\nexport const getUser = createContract('User#getUser')\n  .params('id')\n  .schema({\n    id: V.number(),\n  })\n  .fn(async id =\u003e {\n    return {\n      id,\n      username: 'name',\n    };\n  })\n  .express({\n    auth: true,\n    method: 'get',\n    path: '/users/me',\n    async handler(req, res) {\n      res.json(await getUser((req as any).user.id));\n    },\n  })\n  .express({\n    method: 'get',\n    path: '/users/:id',\n    async handler(req, res) {\n      res.json(await getUser(req.params.id));\n    },\n  });\n\n// Main entry point\n// app.ts\n\nconst app = express();\n\nconst authMiddleware = (req: Request, res: Response) =\u003e {\n  // check if user is logged in\n};\n\ngetUser.expressOptions.forEach(options =\u003e {\n  const middleware: Handler[] = [\n    (req, res, next) =\u003e {\n      options.handler(req, res).catch(next);\n    },\n  ];\n  if (options.auth) {\n    middleware.unshift(authMiddleware);\n  }\n  app[options.method](options.path, ...middleware);\n});\n```\n\n## Using Context\n```ts\nimport { initialize } from 'defensive';\n\ninterface Context {\n  foo: string;\n}\n\nconst { createContract, getContext, runWithContext } = initialize\u003cContext\u003e();\n\nconst fn = createContract('myService#fn2')\n  .params()\n  .fn(async () =\u003e {\n    return new Promise(resolve =\u003e\n      // here will be created a new scope in the event loop\n      setTimeout(() =\u003e { \n        const context = getContext();\n        resolve(context.foo);\n      }, 0)\n    );\n  });\nrunWithContext(\n  {\n    foo: 'bar',\n  },\n  async () =\u003e {\n    console.log(await fn()); // returns 'bar'\n  }\n);\n```\n```\n$ ts-node -T examples/example4.ts\nENTER myService#fn2: { }\nEXIT myService#fn2: 'bar'\nbar\n```\nSee example under `examples/example4.ts`. Run it using `npm run example4`.\n\n\n### API\n1. `initialize` - initialize the library.\n```ts\nconst { createContract, runWithContext, getContext, disable } = initialize({\n  // an array of fields to be removed when formatting input or output\n  removeFields: ['password', 'token', 'accessToken'],\n  // true if enable debugEnter and debugExit, it can be disabled in production\n  debug: true,\n  // the object depth when serializing nested object\n  depth: 4,\n  // the max array length to be serialized\n  maxArrayLength: 30,\n  // the function for handling ENTER event\n  // formattedInput is a serialized contract input\n  debugEnter: (signature, formattedInput) =\u003e {\n    console.log(`ENTER ${signature}:`, formattedInput);\n  },\n  // the function for handling EXI event\n  // formattedOutput is a serialized contract output\n  debugExit: (signature, formattedOutput) =\u003e {\n    console.log(`EXIT ${signature}:`, formattedOutput);\n  },\n});\n```\n2. `createContract` - create a new contract.\n```ts\nconst add = createContract('CalcService#add') // the function signature\n  .params('a', 'b') // input parameter names\n  .schema({\n    a: V.number(), // validation schema for each defined param\n    b: V.number(), // names must match\n  })\n  .fn(async (a, b) =\u003e a + b) // the implementation\n```\n3. `runWithContext` - run the given function with a given context.\n```ts\nconst context = { user: { id: 1 } };\nawait runWithContext(context, async () =\u003e {\n  await add(1, 2);\n})\n```\n\n4. `getContext` - get current context or throw an error if not set. The parent function must call `runWithContext`.\n```ts\nconst { createContract, getContext, runWithContext } = initialize\u003cContext\u003e();\nconst fn = createContract('myService#fn2')\n  .params()\n  .fn(async () =\u003e {\n    return new Promise(resolve =\u003e\n      setTimeout(() =\u003e {\n        const context = getContext();\n        resolve(context.foo);\n      }, 0)\n    );\n  });\nawait runWithContext(\n  {\n    foo: 'bar',\n  },\n  async () =\u003e {\n    await fn(); // returns 'bar'\n  }\n)\n```\n\n5. `ContractError` if an error occurs, a `ContractError` will be thrown.  \nIt contains the following properties:\n- `original: Error` - the original error.\n- `entries: MethodEntry[]` - the call stack of all contracts entries. Each entry contains:\n  - `signature: string` - the contract signature.\n  - `input: string` - the serialized input.\n\n\n## FAQ\n1. Why can't I just use [express validator](https://express-validator.github.io/docs/) and write code directly in controllers?  \n\nSuch an approach can work for small apps, but it can complicate things if the application is growing. It's a common scenario when you write the code in one place, and then you must reuse it in another place.  \nFor example:  \nYou create an endpoint `/POST register` for user registration.  \nAfter some time, you must create a command line script that will register a default user.  \nYou can't call the express router from the command line (you can try but it's a hacky solution), and you must either extract logic to common file (util or helper) or duplicate code. The application is much easier to understand if the business operations are organized in contracts/services instead of chaotic helper methods.\n\n2. Why do you recommend to keep bindings and services in a single file?  \n  \nMost of the services are usually small, and there is 1:1 mapping between them and REST endpoints. It can be overwhelming for the developer when adding a new simple endpoint requires editing multiple files (controllers/services/route config).\n\n3. Why bindings are not provided by this library?  \n  \nIt's difficult to create a generic binding that will work well for all users. It's recommended to create a minimal binding that all only needed in your app.\n\n4. Is context stable?  \n  \nYes, it's based on a native nodejs feature.\n\n### License\nMIT","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flstkz%2Fdefensive","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flstkz%2Fdefensive","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flstkz%2Fdefensive/lists"}