Skip to main content

Dero

Fast micro framework for Deno (support native HTTP/2 Hyper and std/http).

Features

  • Fast (try to 1000+ route, your app still fast).
  • Easy to use (inspired by expressjs middleware (req, res, next)).
  • Routing Controller ready.
  • Support Native HTTP/2 server with Hyper (required Deno 1.9 or higher).
Benchmarks

The benchmarks try to 1000 route and call http://localhost:3000/hello999. Example :

  import { dero } from "https://deno.land/x/dero@0.2.2/mod.ts";

  for (let i = 0; i < 1000; i++) {
      dero.get('/hello' + i, (req, res) => {
          res.body('hello route ' + i);
      });
  }

  await dero.listen(3000);
  wrk -t4 -c4 -d10s http://localhost:3000/hello999

Dero (native http)

  Running 10s test @ http://localhost:3000/hello999
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
      Latency    2.73ms   686.31us  12.67ms   83.95%
      Req/Sec    366.39   18.15     404.00    75.75%
  14611 requests in 10.02s, 1.83MB read
  Requests/sec:   1458.55
  Transfer/sec:   186.59KB

Dero (std/http)

  Running 10s test @ http://localhost:3000/hello999
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
      Latency    3.11ms    1.09ms  20.88ms   90.50%
      Req/Sec    324.37    34.39   380.00    79.75%
  12942 requests in 10.02s, 682.49KB read
  Requests/sec:   1291.15
  Transfer/sec:   68.09KB

Oak

  Running 10s test @ http://localhost:3000/hello999
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
      Latency    4.76ms    1.16ms  23.79ms   80.17%
      Req/Sec    210.51    22.25   262.00    81.00%
  8398 requests in 10.02s, 664.11KB read
  Requests/sec:    837.86
  Transfer/sec:    77.73KB

Opine

  Running 10s test @ http://localhost:3000/hello999
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
      Latency    9.43ms   26.38ms 284.45ms   97.61%
      Req/Sec    176.44   31.29   222.00     73.98%
  6916 requests in 10.02s, 364.71KB read
  Requests/sec:    689.97
  Transfer/sec:    36.39KB

Expressjs (nodejs)

  Running 10s test @ http://localhost:3000/hello999
  4 threads and 4 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
      Latency    4.80ms    3.75ms  78.24ms   96.34%
      Req/Sec    220.80    43.89   282.00    80.50%
  8818 requests in 10.04s, 0.97MB read
  Requests/sec:    878.39
  Transfer/sec:    98.65KB

Installation

deno.land

import { dero } from "https://deno.land/x/dero@0.2.2/mod.ts";

nest.land

import { dero } from "https://x.nest.land/dero@0.2.2/mod.ts";

Usage

import { dero } from "https://deno.land/x/dero@0.2.2/mod.ts";

dero
    .get("/hello", (req, res) => {
        res.body("hello");
    })
    .listen(3000);

Usage With Routing Controller

import { dero, Controller, Get } from "https://deno.land/x/dero@0.2.2/mod.ts";

@Controller("/hello")
class HelloController {

    @Get()
    hello() {
        return "hello";
    }
}

dero
    .use({ class: [HelloController] });
    .listen(3000);

// or with middleware and prefix
// dero.use({
//     prefix: "/api/v1",
//     wares: [midd1, midd2],
//     class: [HelloController]
// });

Run

deno run --allow-net yourfile.ts

Note: for now, native http need –unstable flag.

deno run --allow-net --unstable yourfile.ts

Decorator

Controller Decorator

Controller decorator @Controller(path?: string).

...
@Controller("/hello")
class HelloController { }
...

Method Decorator

Method decorator like @Get(path?: string).

Available => @Get, @Post, @Put, @Delete, @Patch, @Head, @Options, @Any, @Trace, @Connect.

...
@Controller("/hello")
class HelloController {

    @Get()
    hello() {
        return "hello";
    }
}
...

Status Decorator

Set status on decorator like @Status(code: number).

...
@Controller("/hello")
class HelloController {

    @Status(201)
    @Post()
    save() {
        return "Created";
    }
}
...

Header Decorator

Set header on decorator @Header(object | fn).

...
@Controller("/hello")
class HelloController {

    @Header({
        "Content-Type": "text/html"
    })
    @Get()
    hello() {
        return "<h1>Hello</h1>";
    }

    @Header((req, res) => {
        let type = req.url.includes(".css") ? "text/css" : "text/plain";
        return { "Content-Type": type };
    })
    @Get()
    hello2() {
        return Deno.readFile(yourpath);
    }
}
...

Middlewares Decorator

Set Middlewares on decorator @Wares(…middlewares).

...
@Controller("/hello")
class HelloController {

    @Wares((req, res, next) => {
        req.foo = "foo";
        next();
    })
    @Get()
    hello(req: HttpRequest) {
        return req.foo;
    }
}
...

Add class controller to dero.use()

...
dero.use({
    // add class controller
    class: [ClassController1, ClassController2],
    // add middlewares
    wares: [midd1, midd2],
    // add prefix url
    prefix: "/api/v1"
})
...

Config (if you want)

...
// this code is example
dero.config({
    useNativeHttp: boolean,                 /* default true */
    useParseUrl: (req: HttpRequest) => any, /* default native */
    useParseQuery: (qs: string) => any,     /* default native */
});
...

Middleware

import { dero, Controller, Get, Wares } from "https://deno.land/x/dero@0.2.2/mod.ts";

@Controller("/hello")
class HelloController {

    // inside handlers use @Wares
    @Wares(midd1, midd2)
    @Get()
    hello() {
        return "hello";
    }
}
// global middleware with dero.use(...middlewares)
dero.use(midd1, midd2);

// the middleware available only HelloController
dero.use({
    wares: [midd1, midd2]
    class: [HelloController]
});

await dero.listen(3000);

HttpRequest

import { HttpRequest } from "https://deno.land/x/dero@0.2.2/mod.ts";

Query

Query http://localhost:3000/hello?name=john

...
@Controller("/hello")
class HelloController {

    @Get()
    hello(req: HttpRequest) {
        console.log(req.query);
        return req.query.name;
    }
}
...

Params

Params example => http://localhost:3000/hello/1

Standart params => /path/:id

Optional params => /path/:id?

Filtered params => /path/image/:title.(png|jpg)

All params => /path/*

...
@Controller("/hello")
class HelloController {

    @Get("/:id")
    hello(req: HttpRequest) {
        console.log(req.params);
        return req.params.id;
    }

    @Get("/:id?")
    helloOptional(req: HttpRequest) {
        return req.params.id || 'no params';
    }

    // only png and jpg extensions.
    @Get("/image/:title.(png|jpg)")
    getImage(req: HttpRequest) {
        return req.params.title;
    }

    @Get("/all/*")
    getAllParams(req: HttpRequest) {
        // log: {"wild":["param1","param2"]}
        return req.params || {};
    }
}
...

Pond

like req.respond, req.pond help sending body, status, headers. req.pond(body, { status, headers }); where body is string, json, Uint8Array, Deno.Reader,

getBaseUrl

function getBaseUrl() return base url as string.

All HttpRequest

interface HttpRequest {
    respond: (r: any) => Promise<void>;
    pond: (body?: TBody | { [k: string]: any } | null, opts?: PondOptions) => Promise<void>;
    proto: string;
    url: string;
    conn: Deno.Conn;
    isHttps: boolean | undefined;
    method: string;
    headers: Headers;
    body: Deno.Reader | null;
    originalUrl: string;
    params: { [k: string]: any };
    _parsedUrl: { [k: string]: any };
    path: string;
    query: { [k: string]: any };
    search: string | null;
    getBaseUrl: () => string;
};

HttpResponse

import { HttpResponse } from "https://deno.land/x/dero@0.2.2/mod.ts";

header: (key?: object | string | undefined, value?: any) => HttpResponse | string | Headers;

...
// key and value
res.header("key1", "value1");

// with object
res.header({ "key2": "value2" });

// multiple header
res.header({
    "key3": "value3",
    "key4": "value4"
});

// get header
console.log(res.header());
// => Headers {
//         "key1":"value1",
//         "key2":"value2",
//         "key3":"value3",
//         "key4":"value4",
//     }

// get header by key
console.log(res.header("key1"));
// => value1

// delete key1
res.header().delete("key1");
console.log(res.header());
// => Headers {
//         "key2":"value2",
//         "key3":"value3",
//         "key4":"value4",
//     }

// convert to json object
console.log(Object.fromEntries(res.header().entries()));
// => {
//       "key2":"value2",
//       "key3":"value3",
//       "key4":"value4",
//    }

// reset header
res.header(new Headers());
console.log(res.header());
// => Headers { }
...
@Controller("/hello")
class HelloController {

    @Get()
    hello(req: HttpRequest, res: HttpResponse) {
        res.header({"content-type": "text/plain"}).body("Done");
    }
}
...

Type

Shorthand for res.header(“Content-Type”, yourContentType);

...
@Controller("/hello")
class HelloController {

    @Get()
    hello(req: HttpRequest, res: HttpResponse) {
        res.type("text/html").body("<h1>Done</h1>");
    }
}
...

Status

status: (code?: number | undefined) => HttpResponse | number;

...
// set status
res.status(201);

// get status
console.log(res.status());
// => 201
...
@Controller("/hello")
class HelloController {

    @Post()
    hello(req: HttpRequest, res: HttpResponse) {
        res.status(201).body("Created");
    }
}
...

Body

body: (body?: json | string | Uint8Array | Deno.Reader) => Promise;

...
@Controller("/hello")
class HelloController {

    @Get()
    hello(req: HttpRequest, res: HttpResponse) {
        res.body(json | string | Uint8Array | Deno.Reader);
    }
}
...

Return

Mutate ruturning body.

note: this is example using React as template engine.

// filename server.tsx
import { dero } from "https://deno.land/x/dero@0.2.2/mod.ts";
import * as React from "https://jspm.dev/react@17.0.2";
import * as ReactDOMServer from "https://jspm.dev/react-dom@17.0.2/server";

declare global {
    namespace JSX {
        interface IntrinsicElements {
            [k: string]: any;
        }
    }
}

dero
    .use((req, res, next) => {
        // push logic and mutate body in middleware.
        res.return.push((body) => {
            if (React.isValidElement(body)) {
                res.type("text/html");
                return ReactDOMServer.renderToStaticMarkup(body);
            }
            return;
        });
        next();
    })
    .get("/hello", (req, res) => {
        const nums = [1, 2, 3, 4];
        return (
            <div>
                <h1>List Nums</h1>
                {nums.map(el => <p key={el}>{el}</p>)}
            </div>
        )
    })
    .listen(3000)

Next

Next Function is a function to next step handler (on middleware).

example next

...
.use((req, res, next) => {
   res.locals = {
       username: "dero"
   }
   next();
})
...

Router

Dero support classic router.

...
import { dero, Router } from "https://deno.land/x/dero@0.2.2/mod.ts";

const router = new Router();
router.get("/hello", (req, res) => {
    res.body("hello");
})
dero
    .use({ routes: [router] })
    .listen(3000);

// or with middleware and prefix
// dero.use({
//     prefix: "/api/v1",
//     wares: [midd1, midd2],
//     routes: [router1, router2]
// });
...

listen(opts: number | object, callback?: (err?: Error, opts?: object) => void);

    await dero.listen(3000);
    // or
    const cb = (err, opts) => {
        if (err) console.log(err);
        console.log("Running on server " + opts?.port);
    }
    await dero.listen(3000, cb);
    // or
    await dero.listen({ port: 3000, hostname: 'localhost' }, cb);
    // or https
    await dero.listen({ 
        port: 443,
        certFile: "./path/to/localhost.crt",
        keyFile: "./path/to/localhost.key",
    }, cb);
    // or http/2 need deno 1.9.0 or higher
    await dero.listen({ 
        port: 443,
        certFile: "./path/to/localhost.crt",
        keyFile: "./path/to/localhost.key",
        alpnProtocols: ["h2", "http/1.1"]
    }, cb);

Simple error handling.

...
// error handling
dero.use((err: any, req: HttpRequest, res: HttpResponse, next: NextFunction) => {
    res.status(err.code || 500).body({ message: err.message });
});

// not found error handling
dero.use("*", (req, res, next) => {
    res.status(404).body({ message: `Url ${req.url} not found` });
});
...

The role of dero.use

// controllers or routes object { class?: Array, routes?: Array, prefix?: string, wares?: Array }
use(routerControllers: DeroRouterControllers): this;
// spread array middlewares 
use(...middlewares: Array<THandler | THandler[]>): this;
// prefix string spread array middleware (for serve static here)
use(prefix: string, ...middlewares: Array<THandler | THandler[]>): this;

What Next ?

See examples

License

MIT