{"id":22166091,"url":"https://github.com/alextanhongpin/typescript-node-clean-architecture","last_synced_at":"2026-04-17T03:03:04.059Z","repository":{"id":79115318,"uuid":"161356355","full_name":"alextanhongpin/typescript-node-clean-architecture","owner":"alextanhongpin","description":"Figuring out clean architecture design with TypeScript","archived":false,"fork":false,"pushed_at":"2019-05-18T00:32:10.000Z","size":167,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-24T16:15:11.410Z","etag":null,"topics":["architecture","koa","nodejs","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alextanhongpin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-11T15:42:42.000Z","updated_at":"2021-02-08T12:16:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"efbe90ec-1cc7-4df8-8dd7-64ad690c2836","html_url":"https://github.com/alextanhongpin/typescript-node-clean-architecture","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/alextanhongpin/typescript-node-clean-architecture","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Ftypescript-node-clean-architecture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Ftypescript-node-clean-architecture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Ftypescript-node-clean-architecture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Ftypescript-node-clean-architecture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alextanhongpin","download_url":"https://codeload.github.com/alextanhongpin/typescript-node-clean-architecture/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Ftypescript-node-clean-architecture/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31913078,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"online","status_checked_at":"2026-04-17T02:00:06.879Z","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":["architecture","koa","nodejs","typescript"],"created_at":"2024-12-02T05:18:01.406Z","updated_at":"2026-04-17T03:03:04.041Z","avatar_url":"https://github.com/alextanhongpin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# clean-typescript\n\nA better architecture for microservice.\n\n## Init\n\n```bash\n$ tsc --init\n\nnpm init   # and follow the resulting prompts to set up the project\nnpm i koa koa-router koa-body\nnpm i --save-dev typescript ts-node nodemon\nnpm i --save-dev @types/koa @types/koa-router\n```\n\n## Structure\n\nIn order to avoid `shotgun surgery`, whereby making changes will make us change implementations in different places of the codebase, it is preferable to group similar behaviours close to one another.\n\n```js\nfunction decorator(obj, ...decorators) {\n  return decorators.reduce((o, fn) =\u003e fn(o), obj)\n}\nfunction endpointBuilder({\n  requestParser,\n  statusCode,\n  service,\n  responseParser,\n  middlewares\n}) {\n  return function endpoint(req, res) {\n    try {\n      const ctx = {\n        ...res.locals\n      }\n      const request = requestParser({\n        body: req.body,\n        params: req.params,\n        query: req.query\n      })\n      const response = await service(ctx, decorator(request, ...middlewares))\n      res.status(statusCode()).json(responseParser(response))\n    } catch (error) {\n      return res.status(400).json({\n        error: error.message\n      })\n    }\n  }\n}\nfunction requestParser({\n  body\n}) {\n  return {\n    a: body.a,\n    b: body.b\n  }\n}\nfunction responseParser(response) {\n  return {\n    sum: response\n  }\n}\n// What if service requires a dependency (database, logger etc)?\nfunction sumServiceBuilder(repo) {\n  return async function sumService({\n    a,\n    b\n  }) {\n    const response = a + b\n    await repo(a, b, response)\n    return response\n  }\n}\n\nfunction repositoryBuilder(db) {\n  return async function repository(a, b, sum) {\n    const [result] = await db.query('INSERT INTO sum (a, b, sum) VALUES (?, ?, ?)', [a, b, sum])\n    return result.insertId\n  }\n}\n\nfunction validator(request) {\n  const {\n    a,\n    b\n  } = request\n  if (!isDefined(a)) {\n    throw new Error('a is required')\n  }\n  if (!isDefined(b)) {\n    throw new Error('b is required')\n  }\n  if (typeof a !== 'number' || typeof b !== 'number') {\n    throw new Error('invalid number')\n  }\n  return request\n}\n\nfunction isDefined(a) {\n  return a !== null \u0026\u0026 a !== undefined\n}\nconst getSumEndpointBuilder = function builder(db) {\n  return endpointBuilder({\n    requestParser,\n    statusCode: () =\u003e 200,\n    service: sumServiceBuilder(repositoryBuilder(db)),\n    responseParser,\n    middlewares: [validator]\n  })\n}\nmodule.exports = getSumEndpointBuilder\n```\n\n## Synchronous Decorator\n\n```js\nfunction validate(request) {\n  const {\n    a,\n    b\n  } = request\n  if (!a || !b) {\n    throw new Error('missing fields')\n  }\n  console.log('validated')\n  return request\n}\n\nfunction multiply(request) {\n  const {\n    a,\n    b\n  } = request\n  console.log('multiplied')\n  return {\n    a: a * 10,\n    b: b * 10\n  }\n}\n\nfunction decorator(value, ...fns) {\n  return fns.reduce((acc, fn) =\u003e fn(acc), value)\n}\nconst request = {\n  a: 1,\n  b: 2\n}\nconsole.log('response is:', decorator(request, validate, multiply))\n```\n\n\n## Simplify Type\n\n```ts\n// We don't need to declare an interface here, use a type with declaration merging instead.\nexport interface IConfig {\n\thost: string;\n\tport: number;\n\tsecret: string;\n\tcredential: string;\n}\n\nexport const Config = () =\u003e {\n  return {\n    host: process.env.HOST || 'localhost',\n    port: Number(process.env.PORT || 4040),\n    secret: process.env.SECRET || 'secret',\n    credential: process.env.CREDENTIAL || 'hashed credentials',\n  };\n};\n\n// Now Config is both a type and function.\nexport type Config = ReturnType\u003ctypeof Config\u003e;\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falextanhongpin%2Ftypescript-node-clean-architecture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falextanhongpin%2Ftypescript-node-clean-architecture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falextanhongpin%2Ftypescript-node-clean-architecture/lists"}