loading...
Cover image for Why Was Drash Built?

Why Was Drash Built?

drash_land profile image Drash Land Updated on ・6 min read

Hi, my name is Eric Crooks. I'm one of the founders of Drash. In this article, I talk about what Drash is and why I decided to build it with one of my colleagues. Before diving into this article, know that all statements made are by me and all opinions are my own.

Table of Contents

What is Drash?

Drash is a REST microframework for Deno's HTTP server. It has zero dependencies outside of Deno's Standard Modules. It is designed to help you build your projects quickly -- APIs, SPAs, etc.

Why was Drash built?

Before Deno, I used Node and Express. Express was great, but I found issues with the development experience. Mainly, I had issues with the syntax and the time it took to debug issues. More code meant an exponential increase in duplicated code and debugging time. I wasn't a fan of that fact.

Problem 1: Syntax

Take a look at the code below. You might be familiar with the syntax.

app.get('/', function (req, res) {
  res.send('GET request received')
})

app.post('/', function (req, res) {
  res.send('POST request received')
})

app.put('/', function (req, res) {
  res.send('PUT request received')
})

app.delete('/', function (req, res) {
  res.send('DELETE request received')
})

This syntax is great for small applications. It's not so great for larger applications. I found that a large Express application comes at cost. That cost is the degradation of the developer's experience when writing code. The above code looks great, is easy to write, and helps you get started quickly. However, look at how much code is duplicated.

  • The req and res objects are passed in multiple times for each route you define
  • The / route is defined multiple times
  • The app object is used multiple times

This led me to ask myself the following:

  • Why can't the callback function that is passed into the route handler have the req and res objects readily available using this.req and this.res?
  • Why do I have to keep using app.?
  • Why do I have to define the / route one time for each HTTP method?

I'm lazy and don't want to have to write something multiple times if I don't have to. What I wanted was something like this:

app.route('/')
  .get(() => {
    this.res.send('GET request received')
  })
  .post(() => {
    this.res.send('POST request received')
  })
  .put(() => {
    this.res.send('PUT request received')
  })
  .delete(() => {
    this.res.send('DELETE request received')
  })

That syntax looks much better in my opinion and it's easy to write. You take the app object, define a route, and define the allowed HTTP methods for that route.

Problem 2: Debugging

In a larger Express application, your filesystem might be split up so that your front-end and back-end are organized in a way to help you identify where code lives. For example, your filesystem might look like the following:

app/
|-- assets/
|   |-- js/
|       |-- api.js
|       |-- another-js-file.js
|
|-- views/
|   |-- index.html
|   |-- another-view-file.html
|
|-- src/
|   |-- controllers/
|   |   |-- home-controller.js
|   |   |-- user-controller.js
|   |   |-- order-controller.js
|   |
|   |-- services/
|   |   |-- user-service.js
|   |   |-- order-service.js
|   |
|   |-- routes.js
|
|-- app.js
|-- package.json

Let's take the above example code and see why debugging is an issue with it. Inside of api.js, we see the following code and there's an issue with it:

axios.get("/users/1")
  .then((response) => {
    // code to handle the response
  })
  .catch((error) => {
    // code to handle the error
  });

For some reason, the back-end isn't returning what's expected. So let's take a look at the back-end. All we know is /users/1 is the route we need to start with. So let's go to our routes.js file and see what controller is mapped to the route. We see the following:

app.get('/users/:id', UserControllerGet);
app.post('/users/:id', UserControllerPost);
app.put('/users/:id', UserControllerPut);
app.delete('/users/:id', UserControllerDelete);

Ok, now we know that the UserControllerGet function is mapped to the GET /users/:id route. This looks promising because /users/:id matches the front-end's API call to /users/1. Let's go to the UserControllerGet:

function UserControllerGet(req, res) {
  res.send(userService.getUserDetails(req.params));
}

Now we're at the UserControllerGet function and see that the response being created comes from userService.getUserDetails(). So let's go to that file. Actually, let's not. This is too much sifting through code just to figure out where issues are occurring.

This debugging process led me to ask myself the following:

  • Do I have to go through this process every time I debug something? I have to look at the front-end API call, match the route to one of the routes defined in the routes file, and then go from there?
  • Why can't I just assume /users/* maps to a single UserController file and look in that file?

What I wanted was to remove the routes file completely and have:

// File: user-controller.js

app.route("/users/:id")
  .get(() => {
    this.res.send(userService.getUserDetails(req.params));
  })
  .post(() => {
    // POST code
  })
  .put(() => {
    // PUT code
  })
  .delete(() => {
    // DELETE code
  })

Again, you take the app object, define a route, and define the allowed HTTP methods for that route -- all in a single file.

So, how would I solve the syntax issues? How would I solve the debugging issues? Should I rewrite my code to make it work the way I want to? I mean, Express is unopinionated, so I could do that, but why not just create something that makes more sense? Why go with controllers? Why can't we go with resource-based logic? Some frameworks (e.g., Laravel) use resources and they kind of make sense. I had so many questions and in the end I decided to plan out a new framework with my colleague: Drash.

Solution to the problems

Instead of having app.get(), I figured it'd make sense to have a class with HTTP verbs ...

export class MyController {

  public GET() { ... }

  public POST() { ... }

  public PUT() { ... }

  public DELETE() { ... }
}

... and then you'd plug this into your application in some way. I wasn't sure at the time, but I figured it'd look something like:

import { MyController } from "/path/to/my_controller.ts";

const app = new App({
  controllers: [MyController]
})

What about the route definitions? Where do those go? I figured it'd be best to have the controller be in charge of the routes clients can use to target the controller. Like so:

export class MyController {

  public paths = [
    "/my-controller",
    "/my-controller/:some_param"
  ];

  public GET() { ... }

  public POST() { ... }

  public PUT() { ... }

  public DELETE() { ... }
}

With "resources" in the back of my mind and its definition, I figured the controller should just be named a resource. This is how they do it in Tonic (the PHP microframework). Everything in Tonic is a resource. Resources are PHP classes; and resources define their own paths. So now we have something like this:

export class MyResource {

  public paths = [
    "/my-resource",
    "/my-resource/:some_param"
  ];

  public GET() { ... }

  public POST() { ... }

  public PUT() { ... }

  public DELETE() { ... }
}

This looks great! Also, it solves the debugging issue. If I had a front-end that called /my-resource/something, I would already know to go MyResource because the URI should be mapped to a MyResource resource. Same thing goes for a /users API call. /users should map to a UsersResource.

What about the req and res objects? I figured it'd be best to instantiate a resource class and pass in the req and res objects in its constructor. That would give all of the resource class' methods access to the req and res objects without having to pass them in like in Express. This would also mean a BaseResource class would need to be used so that the resource classes that developers define wouldn't require the constructor. So, now we have something like this:

// File: base_resource.ts

export class BaseResource {
  public resource;
  public response;
  constructor(request, response) {
    this.request = request;
    this.response = response;
  }
}
// File: my_resource.ts

import { BaseResource } from "/path/to/base_resource.ts";

export class MyResource extends BaseResource {

  public paths = [
    "/my-resource",
    "/my-resource/:some_param"
  ];

  public GET() {
    this.request.doSomething();
    this.response.doSomething();
  }

  public POST() { ... }

  public PUT() { ... }

  public DELETE() { ... }
}

You might notice the change of the req and res names to request and response. I believe we should be explicit in our code so that newcomers don't question things like variable names.

After a few months of developing around the definition of resources with the syntax being the first thing considered (for a better developer experience), Drash was born. Originally, it was developed with an Express-like syntax, but maintaining that was a nightmare. This was especially true when trying to handle this.request and this.response in chained functions. So, Drash was forced to be different, and I think that's ok because most of its logic is backed by definitions in the MDN.

Hope you enjoyed reading this! I figured it'd be nice to give some insight into Drash.

-- Eric

Posted on by:

Discussion

markdown guide