DEV Community

loading...
Cover image for "And for this interview, build me a custom REST-API router for a web-application"

"And for this interview, build me a custom REST-API router for a web-application"

Thilina Ratnayake
The most non-engineering engineer you'll find :P
・6 min read

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


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 ✨


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

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)

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

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)


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:

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

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

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;
                }
            }
        ]
    }
})
Enter fullscreen mode Exit fullscreen mode

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.

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]
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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" }
}
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
tratnayake profile image
Thilina Ratnayake Author

Hmm, that's a good point! It just need to be one policy that could be applied for all the lambdas to access what they need. I just meant for the fact that for example, if the projects lambda needed to access a different DynamoDB table than the status table.