{"id":13555030,"url":"https://github.com/liamtan28/dactyl","last_synced_at":"2025-05-13T12:53:06.084Z","repository":{"id":56403896,"uuid":"264663056","full_name":"liamtan28/dactyl","owner":"liamtan28","description":"Web framework for Deno, built on top of Oak 🦇","archived":false,"fork":false,"pushed_at":"2024-10-04T00:48:14.000Z","size":329,"stargazers_count":129,"open_issues_count":3,"forks_count":16,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-22T17:49:05.796Z","etag":null,"topics":["dactyl","deno","oak"],"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/liamtan28.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-17T12:41:59.000Z","updated_at":"2025-02-06T02:39:36.000Z","dependencies_parsed_at":"2024-11-04T03:31:43.914Z","dependency_job_id":"6b39afe1-5eb7-4405-943f-2616eff73876","html_url":"https://github.com/liamtan28/dactyl","commit_stats":{"total_commits":151,"total_committers":10,"mean_commits":15.1,"dds":0.5629139072847682,"last_synced_commit":"4641c156cbe829ca5603c7fd06db485882358812"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liamtan28%2Fdactyl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liamtan28%2Fdactyl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liamtan28%2Fdactyl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liamtan28%2Fdactyl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/liamtan28","download_url":"https://codeload.github.com/liamtan28/dactyl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253947879,"owners_count":21988946,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["dactyl","deno","oak"],"created_at":"2024-08-01T12:03:00.189Z","updated_at":"2025-05-13T12:53:06.062Z","avatar_url":"https://github.com/liamtan28.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cimg src=\"./media/fulllogo.jpg?raw=true\" alt=\"dactyl\" width=\"243\" height=\"161\"/\u003e\n\n[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/dactyl/mod.ts) ![LatestRelease](https://img.shields.io/github/v/tag/liamtan28/dactyl)\n![build](https://github.com/liamtan28/dactyl/workflows/build/badge.svg)\n### Web framework for Deno, built on top of Oak\n\n## Currently in the works\n\n1. Dependency injection (delivered and usable! Improvements in the works)\n2. OpenAPI autogeneration of documentation\n3. CLI for autogeneration of Dactyl components\n\n## Available modules:\n\nCurrently, through `mod.ts`, you have access to (docs link on left):\n\n1. [Controller.ts](https://doc.deno.land/https/deno.land/x/dactyl/Controller.ts) - function decorator responsible for assigning controller metadata\n2. [Application.ts](https://doc.deno.land/https/deno.land/x/dactyl/Application.ts) - application class able to register controllers, and start the webserver\n3. [HttpException](https://doc.deno.land/https/deno.land/x/dactyl/HttpException.ts) - throwable exception inside controller actions, `Application` will then handle said errors at top level and send the appropriate HTTP status code and message. There is also a list of included predefined `HttpException` classes, see below\n4. [HttpStatus.ts](https://doc.deno.land/https/deno.land/x/dactyl/HttpStatus.ts) - function decorator responsible for assigning default status codes for controller actions\n5. [Method.ts](https://doc.deno.land/https/deno.land/x/dactyl/Method.ts) - `@Get, @Post, @Put, @Patch, @Delete` function decorators responsible for defining routes on controller actions\n6. [Before.ts](https://doc.deno.land/https/deno.land/x/dactyl/Before.ts) - `@Before` method decorator responsible for defining actions to execute before controller action does. Has access to arguments as follows: `@Before((body, params, query, headers, context) =\u003e console.log('do something!')`\n\n_For following - [Arg.ts](https://doc.deno.land/https/deno.land/x/dactyl/Arg.ts)_\n\n6. `@Param` decorator maps `context.params` onto argument in controller action (returns whole `params` object if no key specified)\n7. `@Body` decorator maps `context.request` async body onto argument in controller action (returns whole `body` object if no key specified)\n8. `@Query` - maps `context.url.searchParams` onto argument in controller action (returns whole `query` object if no key specified)\n9. `@Header` - maps `context.headers` onto argument in controller action (returns whole `header` object if no key specified)\n10. `@Context` - return whole Oak `RouterContext` object\n11. `@Request` - return whole Oak `Request` object\n12. `@Response` - return whole Oak `Response` object\n13. `@Inject` - inject a dependency directly from DIContainer. specify key `@Inject(\"DinosaurService\")`\n\n14. [Router.ts](https://doc.deno.land/https/deno.land/x/dactyl/Router.ts) - It is recommended that you use the `Application` to bootstrap, but you can use the `Router`\n    class directly. This is a superclass of Oak's router, and exposes additional methods for mapping `Controller` definitions onto routes.\n\n15. [Injectable.ts](https://doc.deno.land/https/deno.land/x/dactyl/injectable.ts) - tag a service as injectable. Supply a scope, e.g. `@Injectable(EInjectionScope.SINGLETON)`\n\n## Purpose\n\nDeno is the new kid on the block, and Oak seems to be paving the way for an express-like middleware and routing solution with our fancy new runtime. It's only natural that abstractions on top of Oak are born in the near future - much like Nest tucked express middleware and routing under the hood and provided developers with declarative controllers, DI, etc. This project aims to provide a small portion of these features with room to expand in future.\n\n## Getting started\n\nThis repo contains an example project with one controller. You can execute this on your machine easily with Deno:\n\n`deno run --allow-net --config=tsconfig.json https://deno.land/x/dactyl/example/index.ts`\n\nOne caveat is to ensure you have a `tsconfig.json` file enabling `Reflect` and function decorators for this project, as Deno does not support this in it's default config. Ensure a `tsconfig.json` exists in your directory with at minimum:\n\n```json\n{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true\n  }\n}\n```\n\nThis should result in the following output:\n\n```\n______           _         _\n|  _  \\         | |       | |\n| | | |__ _  ___| |_ _   _| |\n| | | / _` |/ __| __| | | | |\n| |/ / (_| | (__| |_| |_| | |\n|___/ \\__,_|\\___|\\__|\\__, |_| FRAMEWORK\n                      __/ |\n                      |___/\n\n/dinosaur\n  [GET] /\n  [GET] /:id\n  [POST] /\n  [PUT] /:id\n  [DELETE] /:id\n\nDactyl running - please visit http://localhost:8000/\n```\n\nYou can now visit your API.\n\n## Dactyl in action\n\nIn the above example project, there exists one `Controller` and a bootstrapping file, `index.ts` that starts the web server.\n\n`DinosaurController.ts`\nControllers are declared with function decorators. This stores metadata that is consumed on bootstrap and converted into route definitions that Oak can understand.\n\n```ts\n@Controller(\"/dinosaur\", EInjectionScope.SINGLETON)\nclass DinosaurController {\n  constructor(private dinosaurService: DinosaurService) {}\n\n  @Get(\"/\")\n  @HttpStatus(200)\n  getDinosaurs(@Query(\"orderBy\") orderBy: string) {\n    const dinosaurs: Array\u003cany\u003e = this.dinosaurService.getAll();\n    return {\n      message: \"Action returning all dinosaurs! Defaults to 200 status!\",\n      data: dinosaurs,\n    };\n  }\n\n  @Get(\"/:id\")\n  getDinosaurById(@Param(\"id\") id: string, @Header(\"content-type\") contentType: string) {\n    const dinosaur: any = this.dinosaurService.getById(id);\n    return {\n      dinosaur,\n      ContentType: contentType,\n    };\n  }\n\n  @Post(\"/\")\n  createDinosaur(\n    @Body(\"name\") name: any,\n    @Inject(\"DinosaurService\") dinosaurService: DinosaurService\n  ) {\n    if (!name) {\n      throw new BadRequestException(\"name is a required field\");\n    }\n    const newDinosaur: any = dinosaurService.addDinosaur(name);\n    return {\n      message: `Created dinosaur with name ${name}`,\n      newDinosaur,\n    };\n  }\n\n  @Put(\"/:id\")\n  @Before((body: any, params: any) =\u003e {\n    if (!body.name || !params.id) {\n      throw new BadRequestException(\"Caught bad request in decorator\");\n    }\n  })\n  @Before(\n    async () =\u003e\n      await new Promise((resolve: Function) =\u003e\n        setTimeout((): void =\u003e {\n          console.log(\"Can add async actions here too!\");\n          resolve();\n        }, 2000)\n      )\n  )\n  updateDinosaur(@Param(\"id\") id: any, @Body() body: any) {\n    return {\n      message: `Updated name of dinosaur with id ${id} to ${body.name}`,\n    };\n  }\n\n  @Delete(\"/:id\")\n  deleteDinosaur(\n    @Context() ctx: RouterContext,\n    @Request() req: OakRequest,\n    @Response() res: OakResponse\n  ) {\n    res.status = 404;\n    res.body = {\n      msg: `No dinosaur found with id ${ctx.params.id}`,\n    };\n  }\n}\n\nexport default DinosaurController;\n```\n\n`DinosaurService.ts`\nDactyl supports dependency injection, and injects services via the constructor of Controller (see above). Supplied in the example is a service with scope `SINGLETON`, although `TRANSIENT` and `REQUEST` scopes are also supported. You can read more about dependency injection below.\n\n```ts\n@Injectable(EInjectionScope.SINGLETON)\nexport default class DinosaurService {\n  #dinosaurs: Array\u003cany\u003e = [\n    { id: 0, name: \"Tyrannosaurus Rex\", period: \"Maastrichtian\" },\n    { id: 1, name: \"Velociraptor\", period: \"Cretaceous\" },\n    { id: 2, name: \"Diplodocus\", period: \"Oxfordian\" },\n  ];\n\n  getAll(): Array\u003cany\u003e {\n    return this.#dinosaurs;\n  }\n\n  getById(id: string): any {\n    return this.#dinosaurs[parseInt(id, 10)];\n  }\n\n  addDinosaur(name: string) {\n    const newDinosaur: any = {\n      id: ++this.#lastId,\n      name,\n      period: \"Unknown\",\n    };\n    this.#dinosaurs.push(newDinosaur);\n    return newDinosaur;\n  }\n}\n```\n\n`index.ts`\nThis file bootstraps the web server by registering `DinosaurController` to the `Application` instance. `Application` can then use the `.run()` async method to start the webserver.\n\n```ts\nimport { Application } from \"./deps.ts\";\n\nimport DinosaurController from \"./DinosaurController.ts\";\nimport DinosaurService from \"./DinosaurService.ts\";\n\nconst app: Application = new Application({\n  controllers: [DinosaurController],\n  injectables: [DinosaurService],\n});\n\nawait app.run(8000);\n```\n\nAnd away we go. This spins up a web server using oak with the appropriately registered routes based on your controller definitions.\n\n## Configuration\n\nThere is additional configuration that you can pass to the application upon bootstrap using builder pattern:\n\n```ts\nconst app: Application = new Application({\n  controllers: [DinosaurController],\n  injectables: [DinosaurService],\n});\n\napp.useLogger().useCors().useTiming();\n\nconst PORT = 8000;\n\nawait app.run(PORT);\n```\n\n1. `useCors` - Enables CORS middleware. This sets the following headers to `*` on `context.response`: `access-control-allow-origin`, `access-control-allow-methods`, `access-control-allow-methods`.\n2. `useTiming` - Enables timing header middleware. This sets `X-Response-Time` header on `context.response`.\n3. `useLogger` - Enables per-request logging. The message format is: `00:00:00 GMT+0000 (REGION) [GET] - /path/to/endpoint - [200 OK]`\n\n## Dependency Injection\n\nDactyl uses it's own dependency injection container. You can even access the API for the container itself from the `dependency_container` file exported in `mod.ts`.\nThis container supports three scopes: `SINGLETON`, `REQUEST`, and `TRANSIENT`:\n\n`SINGLETON` scoped dependencies are instantiated when the application starts up. When resolved from the container via autoinjection of constructor arguments, you will\nalways receive the same instance that's cached in the container. Use `SINGLETON` scope where possible.\n\n`REQUEST` scoped dependencies are instantiated when a new request is received. When the request lifetime ends, the request dependency cache is dumped. If two concurrent\nrequests are received by the Dactyl server, both requests will receive their own instance, even if they require the same dependency.\n\n`TRANSIENT` scoped dependencies are instantiated every time they are resolved, meaning every controller or service that consumes a `TRANSIENT` dependency will receive\nit's own instance.\n\nCurrently, Dactyl supports autoinjection of dependencies in the constructor, and parameter injection. In order to do this, the following must be done:\n\n1. Tag your service with the `Injectable` class decorator, with the scope you want:\n\n```ts\n@Injectable(EInjectionScope.SINGLETON)\nclass DinosaurService {}\n```\n\n2. Consume your service in the desired controller. It will be resolved by the container based on it's type name. Be sure to tage the class with the `@AutoInject` decorator to auto inject constructor params.\n\n```ts\n@Controller(\"/dinosaur\", EInjectionScope.REQUEST)\nclass DinosaurController {\n  constructor(private dinosaurService: DinosaurService) {}\n}\n```\n\n3. Supply your `Application` class with the injectable, so that it may register it inside the container:\n\n```ts\nconst app: Application = new Application({\n  controllers: [DinosaurController],\n  injectables: [DinosaurService],\n});\n```\n\nAnd you're all done! `DinosaurService` will be autoinjected into the constructor, with the `TRANSIENT` scope.\n\n### Controller scopes\n\nControllers are treated as any other dependency, meaning they can be scoped (`TRANSIENT`, `REQUEST`, `SINGLETON`). Controllers are `REQUEST` scoped by default. It should be noted that `REQUEST` and `TRANSIENT` scoped controllers are effectively\nthe same scope, as controllers are only created once per request for both `REQUEST` and `TRANSIENT`.\n\n### A Note on Scopes\n\nOne common design trap for Dependency Injection is parent dependencies depending on services with a _smaller_ scope than their own. For example:\n`Service A (Singleton) -\u003e Service B (Request)`\nService A is only instantiated once, so how can it depend on a service that is instantiated every request? Some DI implementations will address this\nby making any service that is required by a singleton also a singleton, however this effectively negates the uses for `TRANSIENT` and `REQUEST`.\n\nInstead Dactyl will perform a task (at resolution) to ensure that children dependencies do not decrease in size of scope, ensuring that the three\nscopes are being used properly. The scope size is as follows:\n`Transient -\u003e Request -\u003e Singleton`.\nAnd so the following is true:\n\n```\nTransient -\u003e Request -\u003e Singleton // will not throw error\nTransient -\u003e Transient // will not throw error\nRequest -\u003e Request -\u003e Singleton // will not throw error\nRequest -\u003e Transient // will throw error\nSingleton -\u003e Request // will throw error\n```\n\nThe same is true for controllers, by the way. Attempting the injection of `TRANSIENT` or `REQUEST` services into a `SINGLETON` controller will result in an error.\n\nWe recommend you use `SINGLETON` scope always as those services are only ever resolved once, when `Application.run()` is called.\n\n## Exceptions\n\nExceptions can be raised at any time in the request lifecycle. `HttpException` allows you to raise a custom exception, or you can\nuse a predefined `HttpException` (listed below):\n\n1. `BadRequestException`\n2. `UnauthorizedException`\n3. `PaymentRequiredException`\n4. `ForbiddenException`\n5. `NotFoundException`\n6. `MethodNotAllowedException`\n7. `RequestTimeoutException`\n8. `UnsupportedMediaTypeException`\n9. `TeapotException`\n10. `UnprocessableEntityException`\n11. `TooManyRequestsException`\n12. `RequestHeaderFieldsTooLargeException`\n13. `InternalServerErrorException`\n14. `NotImplementedException`\n15. `BadGatewayException`\n16. `ServiceUnavailableException`\n17. `GatewayTimeoutException`\n\n[HttpException.ts](https://doc.deno.land/https/deno.land/x/dactyl/HttpException.ts)\n\n## Modules\n\nAll modules are accessible without the example project by referring to them in your `deps.ts` file.\nE.g.\n\n```ts\nexport { Controller, DactylRouter, Get } from \"https://deno.land/x/dactyl/mod.ts\";\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliamtan28%2Fdactyl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fliamtan28%2Fdactyl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliamtan28%2Fdactyl/lists"}