You could configure the route as ANY /{proxy+} so that any HTTP method (GET, POST, PATCH, DELETE) for any routes are directed to the same handler. Alternatively, you could also specify the HTTP method to narrow it down, like POST /{proxy+}.
So...
What can I do so that /auth/ URL goes to the same route as /auth in the API Gateway?
Technically speaking, this solves your problem, but now it is up to you to differentiate routes and know what to do.
As far as I know, this is the only way to achieve it with API Gateway, since according to some RFC out there routes "/auth" and "/auth/" are actually different routes and API Gateway complies with that RFC.
This is what I ended up doing (using the ANY /{proxy+}) and, if it is any help, this is the code I have to handle my routes and know what to do:
// Queue.ts
class Queue<T = any> {
private items: T[];
constructor(items?: T[]) {
this.items = items || [];
}
get length(): number {
return this.items.length;
}
enqueue(element: T): void {
this.items.push(element);
}
dequeue(): T | undefined {
return this.items.shift()!;
}
peek(): T | undefined {
if (this.isEmpty()) return undefined;
return this.items[0];
}
isEmpty() {
return this.items.length == 0;
}
}
export default Queue;
// PathMatcher.ts
import Queue from "./Queue";
type PathMatcherResult = {
isMatch: boolean;
namedParameters: Record<string, string>;
};
const NAMED_PARAMETER_REGEX = /(?!\w+:)\{(\w+)\}/;
class PathMatcher {
static match(pattern: string, path: string): PathMatcherResult {
const patternParts = new Queue<string>(this.trim(pattern).split("/"));
const pathParts = new Queue<string>(this.trim(path).split("/"));
const namedParameters: Record<string, string> = {};
const noMatch = { isMatch: false, namedParameters: {} };
if (patternParts.length !== pathParts.length) return noMatch;
while (patternParts.length > 0) {
const patternPart = patternParts.dequeue()!;
const pathPart = pathParts.dequeue()!;
if (patternPart === "*") continue;
if (patternPart.toLowerCase() === pathPart.toLowerCase()) continue;
if (NAMED_PARAMETER_REGEX.test(patternPart)) {
const [name, value] = this.extractNamedParameter(patternPart, pathPart);
namedParameters[name] = value;
continue;
}
return noMatch;
}
return { isMatch: true, namedParameters };
}
private static trim(path: string) {
return path.replace(/^[\s\/]+/, "").replace(/[\s\/]+$/, "");
}
private static extractNamedParameter(
patternPart: string,
pathPart: string
): [string, string] {
const name = patternPart.replace(NAMED_PARAMETER_REGEX, "$1");
let value = pathPart;
if (value.includes(":")) value = value.substring(value.indexOf(":") + 1);
return [name, value];
}
}
export default PathMatcher;
export { PathMatcherResult };
Then, in my lambda handler, I do:
const httpMethod = event.requestContext.http.method.toUpperCase();
const currentRoute = `${httpMethod} ${event.rawPath}`;
// This will match both:
// GET /products/asdasdasdas
// GET /products/asdasdasdas/
const match = PathMatcher.match("GET /products/{id}", currentRoute);
if (match.isMatch) {
// Here, the id parameter has been extracted for you
const productId = match.namedParameters.id;
}
Of course you can build a registry of routes and their respective handler functions and automate that matching process and passing of parameters, but that is the easy part.