|
| 1 | +# CDK Serverless - LLM Context Guide |
| 2 | + |
| 3 | +This document serves as a comprehensive guide for AI assistants working with the CDK Serverless library. It provides an overview of the library's purpose, architecture, key components, and usage patterns to help generate accurate and contextually appropriate code. |
| 4 | + |
| 5 | +## Library Overview |
| 6 | + |
| 7 | +CDK Serverless is a toolkit for building serverless applications on AWS using the AWS Cloud Development Kit (CDK). It provides higher-level (L3) constructs, utility libraries, and project management features designed to simplify serverless development. |
| 8 | + |
| 9 | +### Core Concepts |
| 10 | + |
| 11 | +1. **Higher-level CDK Constructs**: Pre-configured L3 constructs for common serverless patterns like REST APIs, GraphQL APIs, DynamoDB tables, and authentication. |
| 12 | + |
| 13 | +2. **Lambda Function Utilities**: Helper functions and classes for creating Lambda handlers with standardized error handling, authentication, and typing. |
| 14 | + |
| 15 | +3. **Projen Integration**: Project setup tools for quickly scaffolding serverless applications with best practices. |
| 16 | + |
| 17 | +4. **Testing Utilities**: Tools for unit testing Lambda functions and integration testing deployed applications. |
| 18 | + |
| 19 | +## Key Components |
| 20 | + |
| 21 | +### CDK Constructs (`src/constructs/`) |
| 22 | + |
| 23 | +#### `AssetCdn` |
| 24 | + |
| 25 | +Creates an S3 bucket for asset storage with CloudFront distribution for secure HTTPS asset serving. |
| 26 | + |
| 27 | +```typescript |
| 28 | +const cdn = new AssetCdn(this, 'MyCdn', { |
| 29 | + domainName: 'example.com', |
| 30 | + hostName: 'cdn', |
| 31 | + cors: true, |
| 32 | +}); |
| 33 | +``` |
| 34 | + |
| 35 | +#### `CognitoAuthentication` |
| 36 | + |
| 37 | +Sets up Cognito User Pools and optional Identity Pools for authentication. |
| 38 | + |
| 39 | +```typescript |
| 40 | +const auth = new CognitoAuthentication(this, 'Auth', { |
| 41 | + userPoolName: 'my-users', |
| 42 | + selfSignUp: true, |
| 43 | + emailVerification: true, |
| 44 | + userGroups: ['admin', 'user'], |
| 45 | + identityPool: true, |
| 46 | +}); |
| 47 | +``` |
| 48 | + |
| 49 | +#### `GraphQlApi` |
| 50 | + |
| 51 | +Creates an AWS AppSync GraphQL API with resolvers and authentication. |
| 52 | + |
| 53 | +```typescript |
| 54 | +const api = new GraphQlApi(this, 'Api', { |
| 55 | + apiName: 'MyGraphQLApi', |
| 56 | + stageName: 'dev', |
| 57 | + definitionFileName: 'schema.graphql', |
| 58 | + authentication: auth, |
| 59 | + datastore: table, |
| 60 | + domainName: 'example.com', |
| 61 | + apiHostname: 'api', |
| 62 | +}); |
| 63 | + |
| 64 | +// Add resolvers |
| 65 | +api.addLambdaResolver('Query', 'getItems', handler); |
| 66 | +api.addVtlResolver('Mutation', 'createItem', { |
| 67 | + operation: 'PutItem', |
| 68 | + key: util.dynamodb.toMapValues({ PK: 'ITEM#${context.arguments.id}' }), |
| 69 | + attributeValues: util.dynamodb.toMapValues({ ...context.arguments }), |
| 70 | +}); |
| 71 | +``` |
| 72 | + |
| 73 | +#### `RestApi` |
| 74 | + |
| 75 | +Creates an AWS API Gateway REST API using OpenAPI/Swagger specification. |
| 76 | + |
| 77 | +```typescript |
| 78 | +const api = new RestApi(this, 'Api', { |
| 79 | + apiName: 'MyRestApi', |
| 80 | + stageName: 'dev', |
| 81 | + definitionFileName: 'openapi.yaml', |
| 82 | + authentication: auth, |
| 83 | + datastore: table, |
| 84 | + domainName: 'example.com', |
| 85 | + apiHostname: 'api', |
| 86 | + cors: true, |
| 87 | +}); |
| 88 | +``` |
| 89 | + |
| 90 | +#### `SingleTableDatastore` |
| 91 | + |
| 92 | +Sets up a DynamoDB table following the single-table design pattern. |
| 93 | + |
| 94 | +```typescript |
| 95 | +const table = new SingleTableDatastore(this, 'Table', { |
| 96 | + tableName: 'MyTable', |
| 97 | + design: { |
| 98 | + primaryKey: { |
| 99 | + partitionKey: 'PK', |
| 100 | + sortKey: 'SK', |
| 101 | + }, |
| 102 | + indexes: { |
| 103 | + GSI1: { |
| 104 | + partitionKey: 'GSI1PK', |
| 105 | + sortKey: 'GSI1SK', |
| 106 | + }, |
| 107 | + }, |
| 108 | + }, |
| 109 | + ttlAttribute: 'expires', |
| 110 | +}); |
| 111 | +``` |
| 112 | + |
| 113 | +#### `LambdaFunction` |
| 114 | + |
| 115 | +Extended NodejsFunction with additional configurations and permissions. |
| 116 | + |
| 117 | +```typescript |
| 118 | +const lambda = new LambdaFunction(this, 'MyFunction', { |
| 119 | + entry: 'path/to/handler.ts', |
| 120 | + timeout: Duration.seconds(30), |
| 121 | + environment: { |
| 122 | + TABLE_NAME: table.tableName, |
| 123 | + }, |
| 124 | +}); |
| 125 | + |
| 126 | +// Grant permissions |
| 127 | +lambda.grantDatastoreReadWrite(table); |
| 128 | +lambda.grantUserpool(auth.userPool); |
| 129 | +``` |
| 130 | + |
| 131 | +#### `Workflow` |
| 132 | + |
| 133 | +Creates AWS Step Functions state machine. |
| 134 | + |
| 135 | +```typescript |
| 136 | +const workflow = new Workflow(this, 'MyWorkflow', { |
| 137 | + definitionFileName: 'workflow/definition.asl.json', |
| 138 | +}); |
| 139 | +``` |
| 140 | + |
| 141 | +### Lambda Utilities (`src/lambda/`) |
| 142 | + |
| 143 | +#### HTTP Handler |
| 144 | + |
| 145 | +```typescript |
| 146 | +import { createHttpHandler } from 'cdk-serverless/lambda'; |
| 147 | + |
| 148 | +export const handler = createHttpHandler(async (ctx) => { |
| 149 | + return { |
| 150 | + statusCode: 200, |
| 151 | + body: JSON.stringify({ message: 'Hello World' }), |
| 152 | + }; |
| 153 | +}); |
| 154 | +``` |
| 155 | + |
| 156 | +#### OpenAPI Handler |
| 157 | + |
| 158 | +```typescript |
| 159 | +import { createOpenApiHandler } from 'cdk-serverless/lambda'; |
| 160 | + |
| 161 | +export const handler = createOpenApiHandler<paths['/items']['get']>(async (ctx) => { |
| 162 | + // Type-safe access to parameters |
| 163 | + const limit = ctx.queryParams.limit; |
| 164 | + |
| 165 | + return { |
| 166 | + items: [/* items */], |
| 167 | + }; |
| 168 | +}); |
| 169 | +``` |
| 170 | + |
| 171 | +#### AppSync (GraphQL) Handler |
| 172 | + |
| 173 | +```typescript |
| 174 | +import { createAppSyncHandler } from 'cdk-serverless/lambda'; |
| 175 | + |
| 176 | +export const handler = createAppSyncHandler<GetItemsQuery, GetItemsResult>(async (ctx) => { |
| 177 | + const { limit } = ctx.arguments; |
| 178 | + |
| 179 | + return { |
| 180 | + items: [/* items */], |
| 181 | + }; |
| 182 | +}); |
| 183 | +``` |
| 184 | + |
| 185 | +#### Error Handling |
| 186 | + |
| 187 | +```typescript |
| 188 | +import { BadRequestError, NotFoundError } from 'cdk-serverless/lambda'; |
| 189 | + |
| 190 | +export const handler = createHttpHandler(async (ctx) => { |
| 191 | + if (!ctx.queryParams.id) { |
| 192 | + throw new BadRequestError('Missing ID parameter'); |
| 193 | + } |
| 194 | + |
| 195 | + const item = await getItem(ctx.queryParams.id); |
| 196 | + |
| 197 | + if (!item) { |
| 198 | + throw new NotFoundError('Item not found'); |
| 199 | + } |
| 200 | + |
| 201 | + return item; |
| 202 | +}); |
| 203 | +``` |
| 204 | + |
| 205 | +### Projen Integration (`src/projen/`) |
| 206 | + |
| 207 | +#### ServerlessProject |
| 208 | + |
| 209 | +```typescript |
| 210 | +import { ServerlessProject } from 'cdk-serverless/projen'; |
| 211 | + |
| 212 | +const project = new ServerlessProject({ |
| 213 | + name: 'my-serverless-app', |
| 214 | + defaultReleaseBranch: 'main', |
| 215 | +}); |
| 216 | +``` |
| 217 | + |
| 218 | +#### RestApi Project |
| 219 | + |
| 220 | +```typescript |
| 221 | +import { RestApi } from 'cdk-serverless/projen'; |
| 222 | + |
| 223 | +const project = new ServerlessProject({ |
| 224 | + name: 'my-api', |
| 225 | + defaultReleaseBranch: 'main', |
| 226 | +}); |
| 227 | + |
| 228 | +new RestApi(project, { |
| 229 | + apiName: 'MyApi', |
| 230 | + definitionFile: 'api.yaml', |
| 231 | +}); |
| 232 | +``` |
| 233 | + |
| 234 | +#### GraphQL API Project |
| 235 | + |
| 236 | +```typescript |
| 237 | +import { GraphQlApi } from 'cdk-serverless/projen'; |
| 238 | + |
| 239 | +const project = new ServerlessProject({ |
| 240 | + name: 'my-graphql', |
| 241 | + defaultReleaseBranch: 'main', |
| 242 | +}); |
| 243 | + |
| 244 | +new GraphQlApi(project, { |
| 245 | + apiName: 'MyGraphQLApi', |
| 246 | + definitionFile: 'schema.graphql', |
| 247 | +}); |
| 248 | +``` |
| 249 | + |
| 250 | +### Testing Utilities (`src/tests/`) |
| 251 | + |
| 252 | +#### Lambda REST API Testing |
| 253 | + |
| 254 | +```typescript |
| 255 | +import { LambdaRestUnitTest } from 'cdk-serverless/tests/lambda-test-utils'; |
| 256 | + |
| 257 | +describe('API Handler', () => { |
| 258 | + const test = new LambdaRestUnitTest(handler, { |
| 259 | + headers: { |
| 260 | + 'Content-Type': 'application/json', |
| 261 | + }, |
| 262 | + cognito: { |
| 263 | + username: 'test-user', |
| 264 | + |
| 265 | + groups: ['admin'], |
| 266 | + }, |
| 267 | + }); |
| 268 | + |
| 269 | + it('gets items successfully', async () => { |
| 270 | + const result = await test.call({ |
| 271 | + path: '/items', |
| 272 | + method: 'GET', |
| 273 | + queryParams: { limit: '10' }, |
| 274 | + }); |
| 275 | + |
| 276 | + expect(result.statusCode).toBe(200); |
| 277 | + expect(JSON.parse(result.body)).toHaveProperty('items'); |
| 278 | + }); |
| 279 | +}); |
| 280 | +``` |
| 281 | + |
| 282 | +#### Lambda GraphQL Testing |
| 283 | + |
| 284 | +```typescript |
| 285 | +import { LambdaGraphQLTest } from 'cdk-serverless/tests/lambda-test-utils'; |
| 286 | + |
| 287 | +describe('GraphQL Resolver', () => { |
| 288 | + const test = new LambdaGraphQLTest(handler, { |
| 289 | + cognito: { |
| 290 | + username: 'test-user', |
| 291 | + |
| 292 | + groups: ['admin'], |
| 293 | + }, |
| 294 | + }); |
| 295 | + |
| 296 | + it('resolves getItems query', async () => { |
| 297 | + const result = await test.call({ |
| 298 | + fieldName: 'getItems', |
| 299 | + arguments: { limit: 10 }, |
| 300 | + }); |
| 301 | + |
| 302 | + expect(result).toHaveProperty('items'); |
| 303 | + }); |
| 304 | +}); |
| 305 | +``` |
| 306 | + |
| 307 | +#### Integration Testing |
| 308 | + |
| 309 | +```typescript |
| 310 | +import { IntegTestUtil } from 'cdk-serverless/tests/integ-test-util'; |
| 311 | + |
| 312 | +describe('Integration Tests', () => { |
| 313 | + const util = new IntegTestUtil({ |
| 314 | + region: 'us-east-1', |
| 315 | + apiOptions: { |
| 316 | + baseURL: 'https://api.example.com', |
| 317 | + }, |
| 318 | + authOptions: { |
| 319 | + userPoolId: 'us-east-1_xxxxx', |
| 320 | + userPoolClientId: 'xxxxxxxx', |
| 321 | + identityPoolId: 'us-east-1:xxxxxxxx', |
| 322 | + }, |
| 323 | + datastoreOptions: { |
| 324 | + tableName: 'MyTable', |
| 325 | + }, |
| 326 | + }); |
| 327 | + |
| 328 | + beforeAll(async () => { |
| 329 | + await util. createUser( '[email protected]', {}, [ 'admin']); |
| 330 | + }); |
| 331 | + |
| 332 | + afterAll(async () => { |
| 333 | + await util.cleanupItems(); |
| 334 | + await util. removeUser( '[email protected]'); |
| 335 | + }); |
| 336 | + |
| 337 | + it('tests API with authenticated user', async () => { |
| 338 | + const client = await util. getAuthenticatedClient( '[email protected]'); |
| 339 | + |
| 340 | + const response = await client.get('/items'); |
| 341 | + expect(response.status).toBe(200); |
| 342 | + }); |
| 343 | +}); |
| 344 | +``` |
| 345 | + |
| 346 | +## Common Workflows |
| 347 | + |
| 348 | +### Creating a New Serverless Project |
| 349 | + |
| 350 | +1. Initialize a new ServerlessProject |
| 351 | +2. Add required constructs (RestApi, GraphQlApi, etc.) |
| 352 | +3. Run `npx projen` to generate files |
| 353 | +4. Implement Lambda handlers for API operations |
| 354 | +5. Deploy with `cdk deploy` |
| 355 | + |
| 356 | +### Adding a New API Endpoint |
| 357 | + |
| 358 | +1. Update OpenAPI/GraphQL schema |
| 359 | +2. Run `npx projen` to regenerate models |
| 360 | +3. Create Lambda handler for the new operation |
| 361 | +4. Update API construct with new handler |
| 362 | + |
| 363 | +### Implementing Authentication |
| 364 | + |
| 365 | +1. Add CognitoAuthentication construct |
| 366 | +2. Configure API to use the authentication |
| 367 | +3. Use authentication helpers in Lambda handlers |
| 368 | + |
| 369 | +### Working with DynamoDB |
| 370 | + |
| 371 | +1. Create SingleTableDatastore with desired schema |
| 372 | +2. Grant Lambda functions access to the table |
| 373 | +3. Use DynamoDB client in Lambda handlers |
| 374 | + |
| 375 | +## Best Practices |
| 376 | + |
| 377 | +1. **Single-table Design**: Use one DynamoDB table with carefully designed keys for all entities. |
| 378 | +2. **Type Safety**: Leverage TypeScript and generated types for API operations. |
| 379 | +3. **Error Handling**: Use the provided error classes for consistent error responses. |
| 380 | +4. **Testing**: Write unit tests with the provided testing utilities. |
| 381 | +5. **Authentication**: Properly secure all API endpoints using the authentication helpers. |
| 382 | + |
| 383 | +## Common Pitfalls |
| 384 | + |
| 385 | +1. **Incorrect Permissions**: Ensure Lambda functions have the necessary permissions to access resources. |
| 386 | +2. **Missing Environment Variables**: Set required environment variables for Lambda functions. |
| 387 | +3. **Improper Error Handling**: Use the provided error classes instead of throwing generic errors. |
| 388 | +4. **CDK Version Mismatches**: Ensure AWS CDK version is compatible with cdk-serverless. |
| 389 | + |
| 390 | +## Conclusion |
| 391 | + |
| 392 | +CDK Serverless provides a rich set of tools for building serverless applications on AWS. By understanding its components and usage patterns, you can efficiently generate code that follows best practices and integrates well with the library's features. |
0 commit comments