"And for this interview, build me a custom REST-API router for a web-application"
Tags: node, aws, terraform, devops
Published: 2021-05-18 01:22:13

> Palms are sweaty, knees weak, arms are heavy, 

> There's vomit on his Patagucci already

These are the types of tech interview questions my friends used to tell me about that would make me freeze up. The way it's phrased just seems like such a lot of work and hidden complexity. And, I'm sure there is - if you really wanted to knock it out of the park -- but today, at approximately 4 hours into a task that I found more annoying than complex, I realized I'd done just this (sorta, at a very low level).

[Header Image Source](https://junkee.com/yes-eminem-is-selling-moms-spaghetti-t-shirts-that-you-can-actually-buy/77913)

---


 
## T, why were you creating a custom router?

That's a **great** question, I'm glad you asked 🕺🏽.

**SO**

I'm currently working on a project where we're creating a bunch of babby API's to CRUD (Create, Retrieve, Update, Delete) some things from DynamoDB tables. From a bunch of reasons, not least of which including the fact that **I am the sole engineer**  on this project - I'm trying to ~~win sales, earn bonuses and make hella money~~ move quickly and maintain as little "live-infrastructure" as possible.

Because of this, I came to the following conclusion(s)/decision(s) on how I would proceed:

## TIRED 😰

* Running a node.js webserver (and associated infra and management) to effectively broker CRUD requests to a DynamoDB? 

## WIRED ☕️

* Setting up an AWS API Gateway that would trigger a Lambda to CRUD the required things from DynamoDB **WIRED** ~~We're $erverle$$ baaaabyyyyy~~

## INSPIRED ✨

* [but also doing it all within Terraform so that it's documented, codified and re-usable](https://learn.hashicorp.com/tutorials/terraform/lambda-api-gateway)


---

Anyways, the TL:DR on this is that there's going to be an API Gateway that gets HTTP requests and then sends them to a Lambda function which decides to how to deal with the request before brokering the interaction with DynamoDB.

![API V2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bj44c1q6ulvocrgu98h8.png)

I have a single set of resources `projects` that exist in DynamoDB (in a single `projects`) table, and my Lambda needs to be able to listen to the request and get the things from DynamoDB.

From skimming my original blue-print above, you might think:

> This seems easy enough.

And you'd be right, **if** I only ever had to deal with one entity `projects`. As the project went on, now I have a second entity to deal with: `status`(es?) and more are soon to come.

Originally I'd thought:

> 1 lambda per resource. `/resources` will go to the `resources-lambda`, and `/status` will go to the `status-lambda`. 

![API V2 (1)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/71x1px98itx30fpou8eb.png)
 

However this approach leads to a few issues:

* For every endpoint/lambda, you need to create 3x API gateway references
* For every endpoint/lambda, you need to make more IAM accommodations.
* Deployments would get annoying because I would need to update a specific lambda, or multiple lambdas to implement one feature in the future (i.e. if i needed to add a new field to the `status` which makes use of `projects`)

![59xoe3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ssimtvqkb9mtfa2ufm3p.jpg)

I ultimately decided:

## No, we're going to have the API gateway send all (proxy) traffic to a single lambda ~~1 lambda to rule them all~~ (as a proxy resource), and then the lambda can decide how to handle it.

![API V2 (2)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2rqlqioo69nmliy5y9b.png)
 

---

This is why I needed to create a router, so that my Lambda function could figure out what it's being asked to do before doing the appropriate response. For example, it would have to handle:

* `GET /projects` - get me all projects in the database.
* `GET /projects:name` - get me details on a single project.
* `GET /status`- get me all the status entries in the database.
* `GET /status/:name` - get me the status of a single project in the database.
 
---

Having worked with Node (and specifically Express) before, I knew there existed [a way to specify routes like this](https://expressjs.com/en/guide/routing.html):

```
app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)
})
```

And similarly for Lambda, there seemed to exist a specific node module for this case:

[aws-lambda-router](https://www.npmjs.com/package/aws-lambda-router)

```
import * as router from 'aws-lambda-router'

export const handler = router.handler({
    proxyIntegration: {
        routes: [
            {
                // request-path-pattern with a path variable:
                path: '/article/:id',
                method: 'GET',
                // we can use the path param 'id' in the action call:
                action: (request, context) => {
                    return "You called me with: " + request.paths.id;
                }
            },
            {
                // request-path-pattern with a path variable in Open API style:
                path: '/section/{id}',
                method: 'GET',
                // we can use the path param 'id' in the action call:
                action: (request, context) => {
                    return "You called me with: " + request.paths.id;
                }
            }
        ]
    }
})
```
However, unfortunately - proxy path support is still a WIP :( This would seem to imply that ❌ I wouldn't be able to get at route params like the name in  `GET /projects/:name` **WOMP WOMP** ❌

It's also annoying that if you're using custom node-modules, you have to upload it as a zip every single time (as opposed to being able to code / test live if you're using native / vanilla Node).

---

## Well Lambda, I think it's just you (-r `event` parameters) and me at this point.

This would just mean that I'd need to create my own router, and thankfully ~~obviously?~~, the `event` payload that's passed into a Lambda function by the API gateway [contains all the information we could need.](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html)

Specifically, all you *really* need for a router is three things (to start);

* HTTP Method: `GET`,`POST` etc
* Resource: `projects` || `status`
* Params (a.k.a keys): `:name`

Once I got these pieces extracted out from lambda by doing the following:

```
let method = event.httpMethod
let resource = event.path.split("/")[1]
let key = event.path.split("/")[2]
```

The actual logic of the router wasn't too hard. And I guess, just like in a tech interview -- I came up with 2 "solutions".

## V1 - Switch on 1, add more detail inside
```
let method = event.httpMethod
let resource = event.path.split("/")[1]
let key = event.path.split("/")[2]

switch (resource) {
      case "projects":
        if (key == undefined) {
          body = await dynamo.scan({ TableName: PROJECTS_DB_TABLE }).promise();
        } else {
          let name = key;
          body = await db_get("projects",name)
        }
        break;
      case "status":
        break;
      default:
        body = {
          defaultCase: "true",
          path: event.path,
          resource: event.path.split("/")[1],
        };
        break;
    }
```

This approach was cool because it allowed me to use the `path` as the main selector and then code the logic for the required methods as they came up. 

However.it doesn't... *look* great. On first glance, it looks gross, convoluted, and that's just with a single resource and a single method. Secondly, for any new engineers coming onboard - this doesn't immediately seem like a a router when compared to any previous work they may have done.

Going back to the drawing board, and wanting to get closer to the "gold-standard" I was used to, like in express-router.

I wanted to come up with something that would simply specify:

* Here's the route that we need to handle
     * Here's it's associated handler.

With that in mind, I came up with 

## V2 - Filter on 2 conditions, add more methods as they arise

```
let method = event.httpMethod
let resource = event.path.split("/")[1]
let key = event.path.split("/")[2]

 if (method == "GET" && resource == "projects") {
      body = await db_get(dynamo, "projects", key)
    }
else if (method == "GET" && resource == "status") {
    body = await db_get(dynamo, "status", key)
}
else {
  body = { method, resource, key, message: "not supported at this time" }
}
```

I like this because it's the closest I was able to get to express-router:

```
app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)
})
```

And has the benefit of being concise, and much more recognizable as a router on first-glance.

## Things I'd Improve

I'd probably want to do way more clean-up for an actual interview "REAL" router, but it was still a cool thought exercise. Some definite things I'd want to add / handle:

* The `get-me-all` case is handled by checking for an undefined key. This could probably be guarded for better.
* There's currently no guard against someone adding in more than a 1st level parameter (i.e. `/projects/name/something/else` would still get sent to the DB. Thats not great.
* **THIS IS ALL IN A GIANT IF-ELSE STATEMENT??** That doesn't seem great.
* Limitations: There's no way to do middleware, auth, tracing and a bunch of things that you'd be able to do with express-router (and other routers)


## Conclusion

Routers are just giant if-else statements? Idk - this was fun tho.