Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/secure-rest-api-alias-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aligent/cdk-secure-rest-api": minor
---

Add `aliasPaths` to `SecureRestApiRoute`, allowing a route to be exposed under additional paths with the same methods and integration (e.g. renaming an endpoint without breaking existing consumers).
38 changes: 38 additions & 0 deletions packages/constructs/secure-rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A CDK construct for provisioning an API Gateway REST API secured with API Key au
- Configurable CORS preflight options
- Accepts any CDK `Integration` per route (Lambda, HTTP, Mock, Step Functions, etc.)
- Supports nested, multi-segment route paths (e.g. `rewards/accounts/{accountId}/redeem`)
- Supports alias paths so a route can be exposed under an additional path (e.g. renaming an endpoint without breaking existing consumers)
- Configurable deployment stage via `deployOptions` (stage name defaults to `prod`)

## Installation
Expand Down Expand Up @@ -99,6 +100,42 @@ const api = new SecureRestApi(this, 'Api', {
});
```

### Alias paths

Expose a route under one or more additional paths, useful when renaming an
endpoint without breaking existing consumers: keep the old path as an alias
until callers migrate to the new one, then drop `aliasPaths` once it's safe.

```typescript
const api = new SecureRestApi(this, 'Api', {
apiName: 'my-api',
routes: [
{
path: 'customers',
methods: [HttpMethod.GET],
integration: new LambdaIntegration(customersFunction),
aliasPaths: ['people'], // old path, kept working during migration
},
],
});
```

Alias paths work with nested routes too, as long as the parameter names match:

```typescript
const api = new SecureRestApi(this, 'Api', {
apiName: 'my-api',
routes: [
{
path: 'customers/{id}/addresses',
methods: [HttpMethod.GET],
integration: new LambdaIntegration(addressesFunction),
aliasPaths: ['people/{id}/addresses'],
},
],
});
```

### Custom throttling

```typescript
Expand Down Expand Up @@ -160,6 +197,7 @@ Routes to register on the API. Each route requires:
| `path` | `string` | The resource path; may be nested/multi-segment (leading slash is stripped automatically) |
| `methods` | `HttpMethod[]` | HTTP methods to register on the resource |
| `integration` | `Integration` | Any CDK API Gateway integration |
| `aliasPaths` | `string[]` | Optional. Additional paths that register the same `methods` and `integration` |

### `deployOptions` (StageOptions)

Expand Down
54 changes: 54 additions & 0 deletions packages/constructs/secure-rest-api/lib/secure-rest-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,60 @@ describe("SecureRestApi", () => {
);
});

it("registers alias paths with the same methods and integration", () => {
const { stack } = createStack();
new SecureRestApi(stack, "Api", {
apiName: "my-api",
routes: [
{
path: "customers",
methods: [HttpMethod.GET],
integration: mockIntegration(),
aliasPaths: ["people"],
},
],
});

const template = Template.fromStack(stack);
for (const pathPart of ["customers", "people"]) {
template.hasResourceProperties("AWS::ApiGateway::Resource", {
PathPart: pathPart,
});
}
template.resourcePropertiesCountIs(
"AWS::ApiGateway::Method",
{ HttpMethod: "GET", ApiKeyRequired: true },
2
);
});

it("registers a nested alias path sharing a param name", () => {
const { stack } = createStack();
new SecureRestApi(stack, "Api", {
apiName: "my-api",
routes: [
{
path: "customers/{id}/addresses",
methods: [HttpMethod.GET],
integration: mockIntegration(),
aliasPaths: ["people/{id}/addresses"],
},
],
});

const template = Template.fromStack(stack);
for (const pathPart of ["customers", "people", "{id}", "addresses"]) {
template.hasResourceProperties("AWS::ApiGateway::Resource", {
PathPart: pathPart,
});
}
template.resourcePropertiesCountIs(
"AWS::ApiGateway::Method",
{ HttpMethod: "GET", ApiKeyRequired: true },
2
);
});

it("accepts a LambdaIntegration as route integration", () => {
const { stack } = createStack();
new SecureRestApi(stack, "Api", {
Expand Down
34 changes: 29 additions & 5 deletions packages/constructs/secure-rest-api/lib/secure-rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,30 @@ import { HttpMethod } from "aws-cdk-lib/aws-apigatewayv2";
import { Construct } from "constructs";

export interface SecureRestApiRoute {
/**
* The resource path; may be nested/multi-segment (e.g.
* `rewards/accounts/{accountId}/redeem`). A leading slash is stripped
* automatically.
*/
path: string;

/**
* HTTP methods to register on the resource.
*/
methods: HttpMethod[];

/**
* The CDK API Gateway integration to invoke for each method.
*/
integration: Integration;

/**
* Additional paths that expose the same methods and integration as `path`.
*
* Useful for renaming a route without breaking existing consumers: keep
* the old path as an alias until callers migrate to the new one.
*/
aliasPaths?: string[];
}

export interface SecureRestApiProps {
Expand Down Expand Up @@ -114,11 +135,14 @@ export class SecureRestApi extends Construct {
});

for (const route of routes) {
const resource = this.api.root.resourceForPath(
route.path.replace(/^\//, "")
);
for (const method of route.methods) {
resource.addMethod(method, route.integration, { apiKeyRequired: true });
const paths = [route.path, ...(route.aliasPaths ?? [])];
for (const path of paths) {
const resource = this.api.root.resourceForPath(path.replace(/^\//, ""));
for (const method of route.methods) {
resource.addMethod(method, route.integration, {
apiKeyRequired: true,
});
}
}
}

Expand Down