Skip to main content

Dero

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

License deno.land PRs Welcome deps badge cache badge nest.land

Features

  • Native HTTP/2 support.
  • Controller decorator support.
  • Middleware support.
  • Includes body parser (json and urlencoded).
  • Class Validator (based on class-validator).

See examples

Benchmark

Simple benchmark.

autocannon -c 100 http://localhost:3000/

Pure Deno

Name Req/sec Throughput
Native 21433 2.5 MB
std/http 14569 626 KB

Dero

Name Req/sec Throughput
Dero native 20672 2.3 MB
Dero native 13895 544 KB

Installation

deno.land

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

nest.land

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

Usage

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

@Controller("/user")
class UserController extends BaseController {
  @Get()
  getUser() {
    return "Hello";
  }

  @Get("/:name")
  getUserByName() {
    const { name } = this.request.params;
    return name;
  }
}

class Application extends Dero {
  constructor() {
    super();
    this.use({ class: [UserController] });
  }
}

await new Application().listen(3000, () => {
  console.log("Running on port 3000");
});

Run

deno run --allow-net yourfile.ts

Deploy

deploy to https://deno.com/deploy

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

@Controller("/")
class HelloController extends BaseController {
  @Get()
  hello() {
    return "Hello Deploy";
  }
}

class Application extends Dero {
  constructor() {
    super();
    this.use({ class: [HelloController] });
  }
}

const app = new Application();

app.deploy();

// or manual
// addEventListener('fetch', (evt: any) => {
//     evt.respondWith(app.handleEvent(evt));
// })

Decorator

Controller Decorator

Controller decorator @Controller(path?: string).

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

Method Decorator

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

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

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

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

Status Decorator

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

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

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

Header Decorator

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

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

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

Middlewares Decorator

Set Middlewares on decorator @Wares(…middlewares).

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

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

Inject Decorator

...
class UserService {
    async findAll(){
        const data = await User.findAll();
        return { status: 200, data };
    }
}

@Controller("/user")
class UserController extends BaseController {

    @Inject(UserService)
    private readonly userService!: UserService;

    @Get()
    findAll() {
        return this.userService.findAll();
    }
}
...

View Decorator

requires viewEngine

...
@Controller("/user")
class UserController extends BaseController {

    @View("index")
    @Get()
    index() {
        return {
            title: "Welcome"
        };
    }
}
...

Type Decorator

set content type on decorator.

...
@Controller("/user")
class UserController extends BaseController {

    @Type("html")
    @Get()
    index() {
        return `<h1>Hello</h1>`;
    }
}
...

Validate Decorator

Body validator. see doc class-validator. @Validate(dtoClass, options?);

...
import { 
    BaseController, 
    Controller, 
    Validate, 
    Post,
    Dero
} from "https://deno.land/x/dero@1.2.4/mod.ts";

// class validator
import { 
    validateOrReject,
    IsString, 
    IsEmail 
} from "https://cdn.skypack.dev/class-validator?dts";

// validate user
class User {

    @IsString()
    username!: string;

    @IsEmail()
    email!: string;
}

@Controller("/user")
class UserController extends BaseController {

    // validate decorator
    @Validate(User)
    @Post()
    save() {
        return "Success save"
    }
}

class Application extends Dero {
    constructor() {
        super({
            // register class validator
            classValidator: validateOrReject
        });

        // add class controller
        this.use({ class: [UserController] });
    }
}

await new Application().listen(3000);
...

Config

...
class Application extends Dero {
    constructor() {
        super({ 
            nativeHttp: true,
            env: "development",
            parseQuery: qs.parse,
            bodyLimit: {
                json: "3mb",
                urlencoded: "3mb",
                raw: "3mb",

                // to disable body parser please set 0
                // json: 0,
                // urlencoded: 0,
                // raw: 0,
            }
        })

        // more
    }
}
...

Middleware

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

    // inside handlers use @Wares
    @Wares(midd1, midd2)
    @Get()
    hello() {
        return "hello";
    }
}

class Application extends Dero {
    constructor() {
        super();

        // global middleware with .use(...middlewares)
        this.use(midd1, midd2);

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

HttpRequest

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

request.query

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

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

    @Get()
    hello() {
        return this.request.query;
    }
}
...

request.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 extends BaseController {

    @Get("/:id")
    hello() {
        const { id } = this.request.params;
        return id;
    }

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

    // only png and jpg extensions.
    @Get("/image/:title.(png|jpg)")
    getImage() {
        const { title } = this.request.params;
        return title;
    }

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

request.parsedBody

output body but was parsed to json.

request.getBaseUrl

get base url

request.pond

like request.respond.

...
    @Get()
    hello() {
        this.request.pond("hello", {
            status: 200,
            headers: new Headers()
        })
    }
...

request.getCookies

get request cookies

...
    @Get()
    withCookies() {
        const cookies = this.request.getCookies();
        return cookies;
    }

    @Get()
    withCookiesDecode() {
        const cookies = this.request.getCookies(true);
        return cookies;
    }
...

All HttpRequest

class HttpRequest {
  respond!: (r: any) => Promise<void>;
  parsedBody!: { [k: string]: any };
  pond!: (
    body?: TBody | { [k: string]: any } | null,
    opts?: PondOptions,
  ) => Promise<void>;
  getCookies!: () => Record<string, any>;
  proto!: string;
  url!: string;
  conn!: Deno.Conn;
  secure: boolean | undefined;
  bodyUsed: 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@1.2.4/mod.ts";

response.header

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

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

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

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

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

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

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

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

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

    @Get()
    hello() {
        const res = this.response;
        res.header("content-type", "text/html").body("<h1>Done</h1>");
    }
}
...

response.type

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

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

    @Get()
    hello() {
        const res = this.response;
        res.type("html").body("<h1>Done</h1>");
    }
}
...

response.status

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

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

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

    @Post()
    hello() {
        const res = this.response;
        res.status(201).body("Created");
    }
}
...

response.body

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

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

    @Get()
    hello() {
        const res = this.response;
        res.body(json | string | Uint8Array | Deno.Reader);
    }
}
...

response.json

response.json: (jsonData) => Promise;

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

    @Get()
    hello() {
        const res = this.response;
        res.json({ name: "john" });
    }
}
...

response.file

response.file: (pathfile: string, options?) => Promise;

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

    @Get()
    hello() {
        this.response.file("path/to/file.txt");
    }

    @Get("/test")
    hello2() {
        this.response.file("path/to/file.txt", {
            etag: true,
            basedir: Deno.cwd() + "/myfolder"
        });
    }
}
...

response.download

response.download: (pathfile: string, options?) => Promise;

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

    @Get()
    hello() {
        this.response.download("path/to/file.txt");
    }

    @Get("/test")
    hello2() {
        this.response.download("path/to/file.txt", {
            filename: "myCustomFileName.txt",
            basedir: Deno.cwd() + "/myfolder"
        });
    }
}
...

response.cookie

response.cookie: (name, value, options?) => this;

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

    @Get()
    hello() {
        const res = this.response;
        res.cookie("session", "admin", { encode: true, maxAge: 20000 }).body("hello");
    }

    @Get("/home")
    home() {
        const req = this.request;
        res.body(req.getCookies());

        // decode if encode true
        res.body(req.getCookies(true));
    }
}
...

response.clearCookie

response.clearCookie: (name) => void;

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

    @Get()
    hello() {
        const res = this.response;
        res.clearCookie("session");
        res.body("hello");
    }
}
...

response.redirect

response.redirect: (url, status?) => Promise;

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

    @Get()
    hello() {
        this.response.body("Hello")
    }

    @Get("/redirect")
    redirect() {
        this.response.redirect("/hello")
    }
}
...

response.view

response.view: (pathfile, params, …args) => Promise

requires viewEngine

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

    @Get()
    hello() {
        this.response.view("index", {
            name: "john"
        })
    }

}
...

response.return

Mutate ruturning body midlleware.

note: this is example using React as template engine.

// server.tsx
...
import * as React from "https://jspm.dev/react@17.0.2";
import * as ReactDOMServer from "https://jspm.dev/react-dom@17.0.2/server";

@Controller("/hello")
class HelloController extends BaseController {

    @Get()
    hello() {
        const nums = [1, 2, 3, 4];
        return (
            <div>
                <h1>List Nums</h1>
                {nums.map(el => <p key={el}>{el}</p>)}
            </div>
        )
    }
}

class Application extends Dero {
    constructor() {
        super();
        this.use((req, res, next) => {
            // push logic and mutate body in middleware.
            res.return.push((body) => {
                if (React.isValidElement(body)) {
                    res.type("html");
                    return ReactDOMServer.renderToStaticMarkup(body);
                }
                return;
            });
            next();
        });
        this.use({ class: [UserController] });
    }
}
 
await new Application().listen(3000, () => {
    console.log("Running on port 3000")
})
...

All HttpResponse

class HttpResponse {
  locals: any;
  opts!: PondOptions;
  header!: (
    key?: { [k: string]: any } | string,
    value?: any,
  ) => this | (this & Headers) | (this & string);
  status!: (code?: number) => this | (this & number);
  type!: (contentType: string) => this;
  body!: (body?: TBody | { [k: string]: any } | null) => Promise<void>;
  json!: (body: { [k: string]: any } | null) => Promise<void>;
  file!: (
    pathfile: string,
    opts?: { etag?: boolean; basedir?: string },
  ) => Promise<void>;
  download!: (
    pathfile: string,
    opts?: { basedir?: string; filename?: string },
  ) => Promise<void>;
  redirect!: (url: string, status?: number) => Promise<void>;
  clearCookie!: (name: string) => void;
  cookie!: (name: string, value: any, opts?: Cookie) => this;
  view!: (
    name: string,
    params?: Record<string, any>,
    ...args: any
  ) => Promise<void>;
  return!: ((body: any) => any)[];
}

Next

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

example next

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

Classic

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

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

Router

Dero support classic router.

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

const app = new Dero();
const router = new Router();
router.get("/hello", (req, res) => {
    res.body("hello");
})

app.use({ routes: [router] }).listen(3000);

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

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

await app.listen(3000);
// or
const cb = (err, opts) => {
  if (err) console.log(err);
  console.log("Running on server " + opts?.port);
};
await app.listen(3000, cb);
// or
await app.listen({ port: 3000, hostname: "localhost" }, cb);
// or https
await app.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 app.listen({
  port: 443,
  certFile: "./path/to/localhost.crt",
  keyFile: "./path/to/localhost.key",
  alpnProtocols: ["h2", "http/1.1"],
}, cb);

Simple error handling.

...
// error handling
this.onError((err, req, res, next) => {
    res.status(err.code || 500).body({ message: err.message });
});

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

throw error

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

@Controller("/hello")
class HelloController extends BaseController {

    @Get()
    hello() {
        const data = findData();
        if (!data) {
            throw new BadRequestError("Bad data")
        }
        return data;
    }

}
...

Template engine

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

import nunjucks from "https://deno.land/x/nunjucks@3.2.3/mod.js";

@Controller("/user")
class UserController extends BaseController {
  @View("index")
  @Get()
  findAll() {
    return {
      param: "example",
    };
  }
}

// nunjucks configure set basedir views.
nunjucks.configure("views", {/* other config */});

class Application extends Dero {
  constructor() {
    super({
      viewEngine: {
        render: nunjucks.render,
      },
    });

    this.use({ class: [UserController] });
  }
}

// run
await new Application().listen(3000, () => {
  console.log("Running on port 3000");
});

The role of umiddleware (.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