Danet Framework

repository·main·Indexed 19 days ago

https://github.com/savory/danet

A TypeScript framework for Deno inspired by NestJS, designed for professional, scalable, and testable server-side applications. Powered by Hono, Danet provides a structured architecture using modules, controllers, and dependency injection. It includes a CLI for project scaffolding, built-in support for HTTP exception filters, lifecycle hooks (OnAppBootstrap, OnAppClose), and pluggable transports for protocols like gRPC.

Tokens
7.4K
Snippets
28
Records
39
Agent score
68%

What's inside Danet

  1. Overview of Danet

    main

    Danet is a TypeScript framework designed for building efficient and scalable server-side applications on Deno.

    Key characteristics:

    • Inspired by NestJS: It adopts a professional application architecture (originally inspired by Angular) to promote highly testable, scalable, and loosely coupled code.
    • Powered by Hono: Under the hood, Danet utilizes Hono for its underlying web capabilities.
    • Abstraction-focused: It abstracts low-level details so developers can focus on core business logic and architecture.
  2. Install the Danet CLI

    main

    To start building applications with Danet, install the CLI globally using Deno. The CLI allows you to scaffold new projects easily.

    Note: Since version 2.4.0, Danet is distributed via JSR at @danet/core to support runtime agnosticism.

    deno install --global -A -n danet jsr:@danet/cli
  3. How external transports work

    main

    Danet supports pluggable transports (like gRPC) that can take ownership of specific controllers. If a controller carries a specific metadata key, it will be delegated to the registered transport instead of the default HTTP or WebSocket routers.

    To use an external transport:

    1. Implement the TransportRouter interface.
    2. Register it using useTransport(metadataKey, transport) before calling init().
    export interface TransportRouter {
    	registerController(Controller: Constructor, metadataValue: unknown): void;
    }
    
    // Usage
    app.useTransport('grpc-metadata-key', myGrpcTransport);
    await app.init(FirstModule);
  4. Understand ExecutionContext in Danet

    main

    The ExecutionContext is a specialized version of Hono's HttpContext used throughout the Danet request lifecycle. It provides additional metadata about the current execution state, allowing you to access the controller class, the specific handler method being executed, and unique request identifiers. It also supports extensions for WebSocket and gRPC contexts.

    Key properties available on ExecutionContext:

    • _id: A unique identifier for the current execution context.
    • getClass(): Returns the constructor of the controller class handling the request.
    • getHandler(): Returns the function/method currently being executed.
    • websocket: (Optional) The WebSocketInstance if the request is a WebSocket connection.
    • grpcPayload, grpcMetadata, grpcCall: (Optional) Data related to gRPC calls.
    export type ExecutionContext = HttpContext & {
    	_id: string;
    	getHandler: () => Function;
    	getClass: () => Constructor;
    	websocket?: WebSocketInstance;
    	websocketMessage?: any;
    	websocketTopic?: string;
    	grpcPayload?: any;
    	grpcMetadata?: any;
    	grpcCall?: any;
    };
  5. Use parameter decorators to access request data

    main

    Danet provides several parameter decorators to inject request-related data directly into your controller method arguments. These decorators allow you to access the request, response, headers, body, query parameters, URL parameters, session, and the full execution context.

    Common decorators include:

    • @Req(): Injects the current request object.
    • @Res(): Injects the current response object.
    • @WebSocket(): Injects the current WebSocket instance.
    • @Header(prop?: string): Injects all headers or a specific header by name.
    • @Body(prop?: string): Injects the request body or a specific property from the body. If a DTO class is provided as the type hint for the parameter, Danet will automatically validate the body against it.
    • @Query(param?: string, options?: QueryOption): Injects query parameters. You can specify a specific key or retrieve all query parameters. Use options.value to control how multiple values for the same key are handled ('first', 'last', or 'array').
    • @Param(paramName: string): Injects a specific URL parameter (e.g., from /user/:userId).
    • @Session(prop?: string): Injects the session object or a specific property from the session.
    • @Context(): Injects the full ExecutionContext.
    @Controller('/user')
    class UserController {
      @Get('/:userId')
      async getUser(
        @Param('userId') userId: string, // URL param
        @Query('filter', { value: 'array' }) filter: string[], // Query param with array option
        @Body() body: CreateUserDto, // Validated body
        @Req() req: Request,
        @Res() res: Response
      ) {
        // ...
      }
    }
  6. Bootstrap a Danet application

    main

    To start a Danet application, instantiate DanetApplication, initialize it with a root Module, and then call listen() to start the server. The init() method bootstraps the module, resolves dependencies via the injector, and executes the APP_BOOTSTRAP hook for all injectables.

    import {
    	Controller,
    	DanetApplication,
    	Get,
    	Module,
    	Param,
    } from '../src/mod.ts';
    
    @Controller('')
    class FirstController {
    	@Get('hello-world/:name')
    	getHelloWorld(@Param('name') name: string) {
    		return `Hello World ${name}`;
    	}
    }
    
    @Module({
    	controllers: [FirstController]
    })
    class FirstModule {}
    
    const app = new DanetApplication();
    await app.init(FirstModule);
    
    const port = Number(Deno.env.get('PORT')) || 3000;
    app.listen(port);
    import {
    	Controller,
    	DanetApplication,
    	Get,
    	Module,
    	Param,
    } from '../src/mod.ts';
    
    @Controller('')
    class FirstController {
    	@Get('hello-world/:name')
    	getHelloWorld(@Param('name') name: string) {
    		return `Hello World ${name}`;
    	}
    }
    
    @Module({
    	controllers: [FirstController]
    })
    class FirstModule {}
    
    const app = new DanetApplication();
    await app.init(FirstModule);
    
    const port = Number(Deno.env.get('PORT')) || 3000;
    app.listen(port);
  7. Configure ThrottlerOptions for rate limiting

    main

    When setting up a throttler, use the ThrottlerOptions interface to define the rate limiting rules. You can configure multiple throttlers to handle different scenarios, such as short bursts versus long-term sustained limits. Each throttler requires a ttl (time-to-live in milliseconds) and a limit (maximum requests allowed within that window). You can optionally provide a name to identify the throttler; if omitted, it defaults to default.

    // Example configuration for a single throttler
    const options: ThrottlerOptions = {
      name: 'burst',
      ttl: 1000,
      limit: 5,
    };
  8. Disable logging using the NO_LOG environment variable

    main

    You can prevent the Logger class from printing any messages to the console by setting the NO_LOG environment variable in your Deno environment. When this variable is present, all calls to .log(), .warn(), and .error() will return without outputting anything.

    # Example of running your application with logging disabled
    NO_LOG=true deno run src/main.ts
  9. Implement Server-Sent Events (SSE) with @SSE

    main

    The @SSE decorator is used to define a method that handles Server-Sent Events. The method should return an EventTarget which is used to dispatch events to the client.

    @SSE('/stream')
    public myMethod(): EventTarget {
      const eventTarget = new EventTarget();
      let id = 0;
      const interval = setInterval(() => {
        if (id >= 4) {
          clearInterval(interval);
          const event = new SSEEvent({
            retry: 1000,
            id: `${id}`,
            data: 'close',
            event: 'close',
          });
          eventTarget.dispatchEvent(event);
          return;
        }
        const event = new SSEEvent({
          retry: 1000,
          id: `${id}`,
          data: 'world',
          event: 'hello',
        });
        eventTarget.dispatchEvent(event);
        id++;
      }, 100);
      return eventTarget;
    }
  10. Register controllers with DanetHTTPRouter

    main

    To expose your controller methods as HTTP routes, use the registerController method on an instance of DanetHTTPRouter. This method iterates through the controller's prototype and registers methods as routes based on their metadata (like @Get(), @Post(), etc.).

    registerController(Controller: Constructor, basePath: string)

    • Controller: The class constructor of your controller.
    • basePath: The base path prefix for all routes within this controller.
    router.registerController(MyController, 'api/v1');