Function rewrites based on HTTP method for REST API

Hello, I’m trying to build out a REST API using lambda functions, with the goal of cleanly separating functionality at function level, and hopefully avoiding excessive routing inside the function handler. For example, a simple REST API might look like

GET /widgets/:id -> getWidgetId // get a widget by id
POST /widgets/ -> postWidget // create new widget
PUT /widgets/:id -> putWidgetId // update a widget with given id
DELETE /widgets/:id -> deleteWidgetId // delete widget by id

(using function name conventions generated by openapi generator, but they could be anything)

I know how to do this with nginx, and with AWS API gateway it seems that you’d use Methods on Resources. However, I can’t seem to find anything in the netlify redirects documentation on how to match the request based on the method, like you would based on country, role, etc.

The only solutions I can see are (1) stand up a proxy nginx server elsewhere and have that rewrite e.g. a DELETE to a POST to the respective netlify endpoint with a method query parameter (preserves function separation but requires an external service) and (2) send all method requests to the same function then dispatch internally based on APIGatewayEvent’s httpMethod (doesn’t require external routing but conflates unrelated logic in each function, not very RESTy).

Ideally I’d want something like

/widgets/:id Method=DELETE /.netlify/functions/deleteWidgetId	200

Is this not possible currently? Is there a better approach for routing these?

seems like placing a proxy in front of netlify is discouraged, so solution (1) is out: [Support Guide] Why not proxy to Netlify?

however, I am getting a little worried that DELETE/PUT also don’t work in a broader way, as per Can't call lambda function with DELETE or PUT methods

EDIT: checked DELETE with netlify dev and it seems to be working ok

I tried using a Method condition and it had a strange effect

from = "/echo"
to = "/.netlify/functions/echo"
status = 200
force = false
conditions = {Method = "GET"}

from = "/echo"
to = "/.netlify/functions/echoPost"
status = 200
force = false
conditions = {Method = "POST"}

This resulted in:

  • 404 on trying to get /echo
  • 405 Method Not Allowed on any other method request to /echo

No output from netlify dev in either case so it’s hard to say whether this is due to an unknown condition breaking the rewrites OR something actually happening with methods under the hood.

Hiya @parkan and welcome to our community!

Could I ask why you don’t just write one function for /widgets that handles POST, GET, DELETE, PUT, etc differently? No routing needed :slight_smile: . I think the customer you refer to in the post where DELETE/PUT weren’t working figured out that they were wrong about that (cf their recent response).

The “Method” condition you created will not do anything as our service doesn’t even parse that line, being as it is not something we’ve implemented. There is no Method-based routing here at present; it will be unlocked by this work if you’d like to sign up for the beta:

I guess your other option is to create a separate function for each call; doesn’t really need any fancy routing, is just a different call for each functionality. I don’t love it personally - I’d use the first pattern instead.

Hi @fool, thanks for the reply.

The high level reason to separate the different verb handlers into their own functions is that they represent separate logic flows – my GET and DELETE handlers don’t share any code aside from env var loading, for example (and even if they did, I would probably abstract it into an import). So having multiple flows in a single function ends up being a kind of mini “lambda-monolith”, which may or may not be an antipattern, depending on who you ask. I am also concerned that if only one “sub-route” has a heavy dependency, it will need to be loaded even when accessing a more lightweight aspect of the same resource.

Besides, in my view, you definitely do end up doing “routing” – switch (event.httpMethod) to dispatch to the correct flow. Not a huge deal, but I’d prefer to keep it out of the code, especially given that other kinds of routing (/widgets/:id vs /widgets/:id/subwidget) do in fact happen in rewrites.

So, to sum it up, I’d like to have the absolute minimum amount of code per function, to keep my source clean, concerns separated, and bundle size that needs to be loaded at every function execution minimized. I’m a little bit surprised that no one has suggested this as an enhancement to Expanding functionality of redirects, given that e.g. aws API gateway supports it (as do most web frameworks). But maybe this is because I am going about things wrong!

All that being said, edge handlers sounds just like what I’m looking for! Will sign up for the beta.

actually, Tom sums it up better than I can:

Serverless monoliths are not without their drawbacks. But these drawbacks are better understood as failures to reap the benefits of having multiple serverless functions as opposed to non-serverless alternatives. First, routing logic still needs to be written in the application. Second, understanding application performance will require application instrumentation to understand performance based application operation as compared to each operation being a separate serverless function. And lastly, the serverless function’s security permissions will need to be scoped to all possible permissions required instead of narrowing permissions on a per serverless function basis.

In short, IMO bundling the operations into a single “function” is only really beneficial if you’re forklifting existing code or if you’re a new dev ramping up on serverless. The real benefits come from finer granularity.

Fair enough! I think edge handlers will be our solution to ~all~ most of these types of workflows that aren’t already handled in our redirects:

  • IP-based different handling
  • Method-based
  • referrer-based
  • user-agent based

I have a suspicion we’ll maybe get cookie-based routing handle before the edge handlers release, but for now, I guess your best bet will be to create every call separately if you need this working today and you’re uninterested in the monolithic implementation. I understand that puts the complexity in how you call the functions rather than how we route them, but my goal is to get you a workflow you can use today.

Before you go any deeper - are you aware that our functions have a max of 10 seconds default runtime (customizable up to only 26 seconds, at a cost)? Your API may not be a good fit for our lambdas unless your operations all reliably return super fast anyway (since we intend our functions for interactive usage - and we know people in browsers don’t wait that long for a response).

1 Like

I am going with the semi-monolith for now :slight_smile:

This API will be backing an SPA so 10s is quite reasonable for our use case – worst case we can optimistically dispatch any 3rd party API calls and return in <10. If we end up needing more resources we’ll ping sales about options for forklifting onto AWS.

Sounds like it’ll work - can’t wait to have a better solution for you :slight_smile:

1 Like