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
andres
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
andres
objects readily available usingthis.req
andthis.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 singleUserController
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
Top comments (6)
Great explanation of your 'flow of thought and realisation'. Definitely earned a follower...and maybe one day an adopter :)
Thank you! The Drash Land team is always looking to improve the codebase and documentation, so don't hesitate to let us know where we can improve. Feedback is always welcomed in Drash Land :).
Thank you very much.
Would it be possible to show a simple SPA example ?
Regards
About to get drash into production...really love the thought behind the concept. Was looking for something exactly like this to try out. Thanks for this wonderful framework
you're very welcome! let us know how it goes!
Nice Article. Excited to try Drash and the ecosystem.