├── .gitignore ├── 01-s3-bucket ├── first-cdk-project │ ├── .gitignore │ ├── .npmignore │ ├── bin │ │ └── first-cdk-project.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ │ └── first-cdk-project-stack.ts │ ├── package-lock.json │ ├── package.json │ ├── test │ │ └── first-cdk-project.test.ts │ └── tsconfig.json └── website │ ├── index.html │ └── main.css ├── 02-lambda-gateway ├── frontend-app │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components.json │ ├── components │ │ └── ui │ │ │ └── button.tsx │ ├── eslint.config.mjs │ ├── lib │ │ └── utils.ts │ ├── next.config.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ └── tsconfig.json └── lambda-gateway-secrets-manager-stack │ ├── .gitignore │ ├── .npmignore │ ├── bin │ └── lambda-gateway.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ ├── lambda-gateway-stack.ts │ └── secrets-stack.ts │ ├── makeRequests.http │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── lambda │ │ └── handler.ts │ └── utils │ │ └── fetchSecret.ts │ ├── test │ └── lambda-gateway.test.ts │ └── tsconfig.json ├── 03-dynamodb-gateway ├── aws-users-api │ ├── .gitignore │ ├── .npmignore │ ├── bin │ │ └── users-api.ts │ ├── cdk.context.json │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ │ ├── dynamodb-stack.ts │ │ └── users-api-stack.ts │ ├── makeRequests.http │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── lambda │ │ │ └── handler.ts │ ├── test │ │ └── users-api.test.ts │ └── tsconfig.json └── nextjs-frontend-app │ ├── .gitignore │ ├── README.md │ ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx │ ├── components.json │ ├── components │ ├── EditUserModal.tsx │ ├── ReactQueryProvider.tsx │ ├── UserCRUD.tsx │ ├── UserForm.tsx │ ├── UserList.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ └── label.tsx │ ├── eslint.config.mjs │ ├── hooks │ └── useUsers.ts │ ├── lib │ ├── api.ts │ ├── types.ts │ └── utils.ts │ ├── next.config.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── vercel.svg │ └── window.svg │ └── tsconfig.json ├── 04-product-management ├── front-end │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── components.json │ ├── components │ │ ├── confirmation-dialog.tsx │ │ ├── image-upload.tsx │ │ ├── product-form.tsx │ │ ├── product-list.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dialog.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ └── textarea.tsx │ ├── eslint.config.mjs │ ├── lib │ │ ├── api.ts │ │ ├── hooks.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ └── tsconfig.json └── product-management │ ├── .gitignore │ ├── .npmignore │ ├── bin │ └── product-management.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ └── product-management-stack.ts │ ├── makeRequests.http │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── lambda │ │ └── products │ │ │ ├── createProduct.ts │ │ │ ├── deleteProduct.ts │ │ │ └── getAllProducts.ts │ └── types │ │ └── product.ts │ ├── test │ └── product-management.test.ts │ └── tsconfig.json ├── 05-first-sqs └── first-sqs-stack │ ├── .gitignore │ ├── .npmignore │ ├── bin │ └── first-sqs.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ └── first-sqs-stack.ts │ ├── makeRequests.http │ ├── package-lock.json │ ├── package.json │ ├── src │ └── lambda │ │ └── handler.ts │ ├── test │ └── first-sqs.test.ts │ └── tsconfig.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | **/node_modules/ 4 | temp* 5 | # Logs 6 | logs/ 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Build outputs and dependencies 13 | dist/ 14 | build/ 15 | .cdk.staging/ 16 | cdk.out/ 17 | node_modules/ 18 | 19 | # Nested directories 20 | **/dist/ 21 | **/build/ 22 | **/.cdk.staging/ 23 | **/cdk.out/ 24 | **/node_modules/ 25 | 26 | # CDK asset staging directory 27 | .cdk.staging 28 | cdk.out 29 | 30 | # Dependency directories 31 | jspm_packages/ 32 | 33 | # OS/System files 34 | .DS_Store 35 | Thumbs.db 36 | ehthumbs.db 37 | Icon? 38 | 39 | # Environment files 40 | .env 41 | .env.local 42 | .env.*.local 43 | 44 | # IDEs and editors 45 | .vscode/ 46 | .idea/ 47 | *.sublime-workspace 48 | *.sublime-project 49 | 50 | # Optional: macOS-specific 51 | DC_STORE 52 | 53 | # Test coverage 54 | coverage/ 55 | .nyc_output/ 56 | 57 | # TypeScript 58 | *.tsbuildinfo 59 | 60 | # Misc 61 | *.tgz 62 | *.gz 63 | *.zip 64 | *.bak 65 | *.tmp 66 | 67 | # Git hooks and other config backups 68 | *.orig 69 | 70 | # Ignore lock files if not using them strictly 71 | # package-lock.json 72 | # yarn.lock -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/bin/first-cdk-project.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { FirstCdkProjectStack } from '../lib/first-cdk-project-stack'; 4 | 5 | const app = new cdk.App(); 6 | new FirstCdkProjectStack(app, 'FirstCdkProjectStack', {}); 7 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/first-cdk-project.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 38 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 39 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 40 | "@aws-cdk/aws-route53-patters:useCertificate": true, 41 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 42 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 43 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 44 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 45 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 46 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 47 | "@aws-cdk/aws-redshift:columnId": true, 48 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 49 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 50 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 51 | "@aws-cdk/aws-kms:aliasNameRef": true, 52 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 54 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 56 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 57 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 58 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 59 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 60 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 61 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 62 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 63 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 64 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 65 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 66 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 67 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 68 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 69 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 70 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 71 | "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, 72 | "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, 73 | "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, 74 | "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, 75 | "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, 76 | "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, 77 | "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, 78 | "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, 79 | "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, 80 | "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, 81 | "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, 82 | "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, 83 | "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, 84 | "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, 85 | "@aws-cdk/core:enableAdditionalMetadataCollection": true, 86 | "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, 87 | "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, 88 | "@aws-cdk/aws-events:requireEventBusPolicySid": true, 89 | "@aws-cdk/core:aspectPrioritiesMutating": true, 90 | "@aws-cdk/aws-dynamodb:retainTableReplica": true, 91 | "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, 92 | "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, 93 | "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, 94 | "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/lib/first-cdk-project-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | // import * as sqs from 'aws-cdk-lib/aws-sqs'; 4 | import * as s3 from 'aws-cdk-lib/aws-s3'; 5 | 6 | export class FirstCdkProjectStack extends cdk.Stack { 7 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | const bucket = new s3.Bucket(this, 'FirstCdkProjectBucket', { 10 | removalPolicy: cdk.RemovalPolicy.DESTROY, 11 | autoDeleteObjects: true, 12 | }); 13 | 14 | const bucket2 = new s3.Bucket(this, 'MySecondCdkBucket', { 15 | bucketName: 'my-second-cdk-bucket-1234', 16 | removalPolicy: cdk.RemovalPolicy.DESTROY, 17 | websiteIndexDocument: 'index.html', 18 | autoDeleteObjects: true, 19 | publicReadAccess: true, 20 | blockPublicAccess: new s3.BlockPublicAccess({ 21 | blockPublicAcls: false, 22 | blockPublicPolicy: false, 23 | ignorePublicAcls: false, 24 | restrictPublicBuckets: false, 25 | }), 26 | }); 27 | // bucket.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); 28 | new cdk.CfnOutput(this, 'BucketName', { 29 | value: bucket.bucketName, 30 | description: 'The name of the bucket', 31 | }); 32 | new cdk.CfnOutput(this, 'BucketName2', { 33 | value: bucket2.bucketName, 34 | description: 'The name of the bucket', 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "first-cdk-project", 3 | "version": "0.1.0", 4 | "bin": { 5 | "first-cdk-project": "bin/first-cdk-project.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.14", 15 | "@types/node": "22.7.9", 16 | "jest": "^29.7.0", 17 | "ts-jest": "^29.2.5", 18 | "aws-cdk": "2.1018.0", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.6.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.199.0", 24 | "constructs": "^10.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/test/first-cdk-project.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as FirstCdkProject from '../lib/first-cdk-project-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/first-cdk-project-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new FirstCdkProject.FirstCdkProjectStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /01-s3-bucket/first-cdk-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "lib": [ 7 | "es2022" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /01-s3-bucket/website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |

CDK IS THE BEST

11 | 12 | 13 | -------------------------------------------------------------------------------- /01-s3-bucket/website/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: gainsboro; 3 | } 4 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/aws-cdk-course/ecbd59766a447a17f7821ac9dc834daf01646e8e/02-lambda-gateway/frontend-app/app/favicon.ico -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.13 0.028 261.692); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.13 0.028 261.692); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.13 0.028 261.692); 54 | --primary: oklch(0.21 0.034 264.665); 55 | --primary-foreground: oklch(0.985 0.002 247.839); 56 | --secondary: oklch(0.967 0.003 264.542); 57 | --secondary-foreground: oklch(0.21 0.034 264.665); 58 | --muted: oklch(0.967 0.003 264.542); 59 | --muted-foreground: oklch(0.551 0.027 264.364); 60 | --accent: oklch(0.967 0.003 264.542); 61 | --accent-foreground: oklch(0.21 0.034 264.665); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.928 0.006 264.531); 64 | --input: oklch(0.928 0.006 264.531); 65 | --ring: oklch(0.707 0.022 261.325); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0.002 247.839); 72 | --sidebar-foreground: oklch(0.13 0.028 261.692); 73 | --sidebar-primary: oklch(0.21 0.034 264.665); 74 | --sidebar-primary-foreground: oklch(0.985 0.002 247.839); 75 | --sidebar-accent: oklch(0.967 0.003 264.542); 76 | --sidebar-accent-foreground: oklch(0.21 0.034 264.665); 77 | --sidebar-border: oklch(0.928 0.006 264.531); 78 | --sidebar-ring: oklch(0.707 0.022 261.325); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.13 0.028 261.692); 83 | --foreground: oklch(0.985 0.002 247.839); 84 | --card: oklch(0.21 0.034 264.665); 85 | --card-foreground: oklch(0.985 0.002 247.839); 86 | --popover: oklch(0.21 0.034 264.665); 87 | --popover-foreground: oklch(0.985 0.002 247.839); 88 | --primary: oklch(0.928 0.006 264.531); 89 | --primary-foreground: oklch(0.21 0.034 264.665); 90 | --secondary: oklch(0.278 0.033 256.848); 91 | --secondary-foreground: oklch(0.985 0.002 247.839); 92 | --muted: oklch(0.278 0.033 256.848); 93 | --muted-foreground: oklch(0.707 0.022 261.325); 94 | --accent: oklch(0.278 0.033 256.848); 95 | --accent-foreground: oklch(0.985 0.002 247.839); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.551 0.027 264.364); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.21 0.034 264.665); 106 | --sidebar-foreground: oklch(0.985 0.002 247.839); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0.002 247.839); 109 | --sidebar-accent: oklch(0.278 0.033 256.848); 110 | --sidebar-accent-foreground: oklch(0.985 0.002 247.839); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.551 0.027 264.364); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | const geistSans = Geist({ 6 | variable: '--font-geist-sans', 7 | subsets: ['latin'], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: '--font-geist-mono', 12 | subsets: ['latin'], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: 'CDK API', 17 | description: 'Practice http requests', 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import axios from 'axios'; 5 | import { useState } from 'react'; 6 | const API_URL = 'https://o7skyi8sba.execute-api.eu-north-1.amazonaws.com/'; 7 | export default function Home() { 8 | const [data, setData] = useState(''); 9 | 10 | const handleGetRequest = async () => { 11 | try { 12 | const response = await axios.get(`${API_URL}`); 13 | console.log(response.data); 14 | setData(response.data.message); 15 | } catch (error) { 16 | setData('Error fetching data'); 17 | console.error('Error:', error); 18 | } 19 | }; 20 | 21 | const handlePostRequest = async () => { 22 | try { 23 | const response = await axios.post(`${API_URL}profile`, { 24 | // Add your post data here 25 | username: 'bobo is the best', 26 | }); 27 | console.log(response.data); 28 | setData(response.data.message); 29 | } catch (error) { 30 | setData('Error posting data'); 31 | console.error('Error:', error); 32 | } 33 | }; 34 | 35 | return ( 36 |
37 |
38 |
{data || 'Data will appear here...'}
39 |
40 |
41 | 44 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-slot": "^1.2.3", 13 | "axios": "^1.9.0", 14 | "class-variance-authority": "^0.7.1", 15 | "clsx": "^2.1.1", 16 | "lucide-react": "^0.513.0", 17 | "next": "15.3.3", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "tailwind-merge": "^3.3.0" 21 | }, 22 | "devDependencies": { 23 | "@eslint/eslintrc": "^3", 24 | "@tailwindcss/postcss": "^4", 25 | "@types/node": "^20", 26 | "@types/react": "^19", 27 | "@types/react-dom": "^19", 28 | "eslint": "^9", 29 | "eslint-config-next": "15.3.3", 30 | "tailwindcss": "^4", 31 | "tw-animate-css": "^1.3.4", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-lambda-gateway/frontend-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/bin/lambda-gateway.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { LambdaGatewayStack } from '../lib/lambda-gateway-stack'; 4 | import { SecretsStack } from '../lib/secrets-stack'; 5 | const app = new cdk.App(); 6 | 7 | const secretsStack = new SecretsStack(app, 'SecretsStack'); 8 | 9 | const lambdaStack = new LambdaGatewayStack(app, 'LambdaGatewayStack', { 10 | secretsStack, 11 | }); 12 | 13 | lambdaStack.addDependency(secretsStack); 14 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/lambda-gateway.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 38 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 39 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 40 | "@aws-cdk/aws-route53-patters:useCertificate": true, 41 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 42 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 43 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 44 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 45 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 46 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 47 | "@aws-cdk/aws-redshift:columnId": true, 48 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 49 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 50 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 51 | "@aws-cdk/aws-kms:aliasNameRef": true, 52 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 54 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 56 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 57 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 58 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 59 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 60 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 61 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 62 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 63 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 64 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 65 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 66 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 67 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 68 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 69 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 70 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 71 | "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, 72 | "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, 73 | "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, 74 | "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, 75 | "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, 76 | "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, 77 | "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, 78 | "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, 79 | "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, 80 | "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, 81 | "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, 82 | "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, 83 | "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, 84 | "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, 85 | "@aws-cdk/core:enableAdditionalMetadataCollection": true, 86 | "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, 87 | "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, 88 | "@aws-cdk/aws-events:requireEventBusPolicySid": true, 89 | "@aws-cdk/core:aspectPrioritiesMutating": true, 90 | "@aws-cdk/aws-dynamodb:retainTableReplica": true, 91 | "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, 92 | "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, 93 | "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, 94 | "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, 95 | "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/lib/lambda-gateway-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | // import * as sqs from 'aws-cdk-lib/aws-sqs'; 4 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 5 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 6 | import * as path from 'path'; 7 | import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; 8 | import * as apigateway_integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; 9 | import { SecretsStack } from './secrets-stack'; 10 | import * as iam from 'aws-cdk-lib/aws-iam'; 11 | 12 | export class LambdaGatewayStack extends cdk.Stack { 13 | private readonly secretsStack: SecretsStack; 14 | constructor(scope: Construct, id: string, props: cdk.StackProps & { secretsStack: SecretsStack }) { 15 | super(scope, id, props); 16 | this.secretsStack = props.secretsStack; 17 | const exampleLambda = new NodejsFunction(this, 'ExampleHandler', { 18 | runtime: lambda.Runtime.NODEJS_22_X, 19 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 20 | handler: 'lambdaExample', 21 | functionName: `${this.stackName}-cdk-course-example-lambda`, 22 | }); 23 | new cdk.CfnOutput(this, 'ExampleLambdaArn', { 24 | value: exampleLambda.functionArn, 25 | description: 'The ARN of the example lambda function', 26 | }); 27 | 28 | const homeLambda = new NodejsFunction(this, 'HomeHandler', { 29 | runtime: lambda.Runtime.NODEJS_22_X, 30 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 31 | handler: 'homeRoute', 32 | functionName: `${this.stackName}-home-route-lambda`, 33 | }); 34 | 35 | const httpApi = new apigateway.HttpApi(this, 'FirstApi', { 36 | apiName: 'First API', 37 | description: 'First API with CDK', 38 | corsPreflight: { 39 | allowOrigins: ['*'], 40 | allowMethods: [apigateway.CorsHttpMethod.ANY], 41 | allowHeaders: ['*'], 42 | }, 43 | }); 44 | httpApi.addRoutes({ 45 | path: '/', 46 | methods: [apigateway.HttpMethod.GET], 47 | integration: new apigateway_integrations.HttpLambdaIntegration('HomeIntegration', homeLambda), 48 | }); 49 | new cdk.CfnOutput(this, 'HttpApiUrl', { 50 | value: httpApi.url ?? '', 51 | description: 'HTTP API URL', 52 | }); 53 | const createProfileLambda = new NodejsFunction(this, 'ProfileHandler', { 54 | runtime: lambda.Runtime.NODEJS_22_X, 55 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 56 | handler: 'createProfileRoute', 57 | functionName: `${this.stackName}-profile-lambda`, 58 | }); 59 | 60 | httpApi.addRoutes({ 61 | path: '/profile', 62 | methods: [apigateway.HttpMethod.POST], 63 | integration: new apigateway_integrations.HttpLambdaIntegration('ProfileIntegration', createProfileLambda), 64 | }); 65 | 66 | const welcomeLambda = new NodejsFunction(this, 'WelcomeHandler', { 67 | runtime: lambda.Runtime.NODEJS_22_X, 68 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 69 | handler: 'welcomeRoute', 70 | functionName: `${this.stackName}-welcome-route`, 71 | environment: { 72 | USERNAME: 'shakeAndBake', 73 | }, 74 | }); 75 | 76 | // more code here 77 | // welcomeLambda.addEnvironment('USERNAME', 'shakeAndBake'); 78 | 79 | httpApi.addRoutes({ 80 | path: '/welcome', 81 | methods: [apigateway.HttpMethod.GET], 82 | integration: new apigateway_integrations.HttpLambdaIntegration('ProfileIntegration', welcomeLambda), 83 | }); 84 | 85 | const loginLambda = new NodejsFunction(this, 'LoginHandler', { 86 | runtime: lambda.Runtime.NODEJS_22_X, 87 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 88 | handler: 'loginRoute', 89 | functionName: `${this.stackName}-login-route-lambda`, 90 | }); 91 | 92 | // more code here 93 | loginLambda.addEnvironment('SECRET_ID', this.secretsStack.secret.secretName); 94 | 95 | loginLambda.addToRolePolicy( 96 | new iam.PolicyStatement({ 97 | effect: iam.Effect.ALLOW, 98 | actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], 99 | resources: [this.secretsStack.secret.secretArn], 100 | }) 101 | ); 102 | 103 | httpApi.addRoutes({ 104 | path: '/login', 105 | methods: [apigateway.HttpMethod.POST], 106 | integration: new apigateway_integrations.HttpLambdaIntegration('LoginIntegration', loginLambda), 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/lib/secrets-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; 4 | 5 | export class SecretsStack extends cdk.Stack { 6 | public readonly secret: secretsmanager.Secret; 7 | 8 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 9 | super(scope, id, props); 10 | this.secret = new secretsmanager.Secret(this, 'MyAppSecret', { 11 | secretName: 'my-app-secret', 12 | secretObjectValue: {}, 13 | }); 14 | this.secret.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); 15 | new cdk.CfnOutput(this, 'SecretArn', { 16 | value: this.secret.secretArn, 17 | description: 'The ARN of the secret', 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/makeRequests.http: -------------------------------------------------------------------------------- 1 | @URL = https://o7skyi8sba.execute-api.eu-north-1.amazonaws.com 2 | 3 | ### Get Home Route 4 | GET {{URL}} 5 | 6 | 7 | 8 | ### Create Profile Route 9 | POST {{URL}}/profile 10 | Content-Type: :application/json 11 | 12 | { 13 | "username":"shakeAndBake" 14 | } 15 | 16 | 17 | ### Get Welcome Route 18 | GET {{URL}}/welcome 19 | 20 | 21 | ### Login Route 22 | POST {{URL}}/login 23 | Content-Type: :application/json 24 | 25 | { 26 | "username":"shakeAndBake" 27 | } -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-gateway", 3 | "version": "0.1.0", 4 | "bin": { 5 | "lambda-gateway": "bin/lambda-gateway.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/aws-lambda": "^8.10.150", 15 | "@types/jest": "^29.5.14", 16 | "@types/node": "22.7.9", 17 | "aws-cdk": "2.1018.1", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.2.5", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.6.3" 22 | }, 23 | "dependencies": { 24 | "@aws-sdk/client-secrets-manager": "^3.839.0", 25 | "aws-cdk-lib": "2.200.1", 26 | "constructs": "^10.0.0", 27 | "esbuild": "^0.25.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/src/lambda/handler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEventV2 } from 'aws-lambda'; 2 | import { fetchSecret } from '../utils/fetchSecret'; 3 | import crypto from 'crypto'; 4 | 5 | export const lambdaExample = async (event: any) => { 6 | console.log('TEMP Event log', event); 7 | return { 8 | message: 'Hello World', 9 | }; 10 | }; 11 | 12 | export const homeRoute = async (event: APIGatewayProxyEventV2) => { 13 | console.log('Home Route Event Log', event); 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({ 17 | message: 'Welcome to the API!', 18 | }), 19 | }; 20 | }; 21 | export const createProfileRoute = async (event: APIGatewayProxyEventV2) => { 22 | console.log('TEMP POST Event', event); 23 | const body = JSON.parse(event.body ?? '{}'); 24 | return { 25 | statusCode: 201, 26 | body: JSON.stringify({ 27 | message: 'Profile created successfully', 28 | username: body.username, 29 | }), 30 | }; 31 | }; 32 | export const welcomeRoute = async (event: APIGatewayProxyEventV2) => { 33 | const username = process.env.USERNAME; 34 | const message = username ? `Welcome ${username}!` : `Welcome to the API!`; 35 | return { 36 | statusCode: 200, 37 | body: JSON.stringify({ 38 | message, 39 | }), 40 | }; 41 | }; 42 | 43 | export const loginRoute = async (event: APIGatewayProxyEventV2) => { 44 | try { 45 | const { username } = JSON.parse(event.body ?? '{}'); 46 | const secretValue = await fetchSecret(process.env.SECRET_ID!); 47 | const { encryptionKey } = JSON.parse(secretValue); 48 | const hashedUsername = crypto.createHmac('sha256', encryptionKey).update(username).digest('hex'); 49 | return { 50 | statusCode: 200, 51 | body: JSON.stringify({ 52 | username: hashedUsername, 53 | }), 54 | }; 55 | } catch (error) { 56 | console.error('Error :', error); 57 | return { 58 | statusCode: 500, 59 | body: JSON.stringify({ 60 | message: 'Something went wrong', 61 | }), 62 | }; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/src/utils/fetchSecret.ts: -------------------------------------------------------------------------------- 1 | import { SecretsManagerClient, GetSecretValueCommand, GetSecretValueCommandOutput } from '@aws-sdk/client-secrets-manager'; 2 | 3 | const secretsClient = new SecretsManagerClient({}); 4 | 5 | export const fetchSecret = async (secretId: string): Promise => { 6 | const command = new GetSecretValueCommand({ 7 | SecretId: secretId, 8 | }); 9 | let response: GetSecretValueCommandOutput; 10 | 11 | try { 12 | response = await secretsClient.send(command); 13 | } catch (error) { 14 | throw new Error('Failed to fetch secret'); 15 | } 16 | if (!response.SecretString) { 17 | throw new Error('Secret value is undefined'); 18 | } 19 | return response.SecretString; 20 | }; 21 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/test/lambda-gateway.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as LambdaGateway from '../lib/lambda-gateway-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/lambda-gateway-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new LambdaGateway.LambdaGatewayStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /02-lambda-gateway/lambda-gateway-secrets-manager-stack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "lib": [ 7 | "es2022" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/bin/users-api.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { UsersApiStack } from '../lib/users-api-stack'; 4 | import { DynamoDBStack } from '../lib/dynamodb-stack'; 5 | const app = new cdk.App(); 6 | 7 | const dynamodbStack = new DynamoDBStack(app, 'DynamoDBStack'); 8 | 9 | const usersApStack = new UsersApiStack(app, 'UsersApiStack', { dynamodbStack }); 10 | usersApStack.addDependency(dynamodbStack); 11 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "acknowledged-issue-numbers": [ 3 | 34892 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/users-api.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 38 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 39 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 40 | "@aws-cdk/aws-route53-patters:useCertificate": true, 41 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 42 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 43 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 44 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 45 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 46 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 47 | "@aws-cdk/aws-redshift:columnId": true, 48 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 49 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 50 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 51 | "@aws-cdk/aws-kms:aliasNameRef": true, 52 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 54 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 55 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 56 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 57 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 58 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 59 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 60 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 61 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 62 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 63 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 64 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 65 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 66 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 67 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 68 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 69 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, 70 | "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, 71 | "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, 72 | "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, 73 | "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, 74 | "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, 75 | "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, 76 | "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, 77 | "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, 78 | "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, 79 | "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, 80 | "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, 81 | "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, 82 | "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, 83 | "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, 84 | "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, 85 | "@aws-cdk/core:enableAdditionalMetadataCollection": true, 86 | "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, 87 | "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, 88 | "@aws-cdk/aws-events:requireEventBusPolicySid": true, 89 | "@aws-cdk/core:aspectPrioritiesMutating": true, 90 | "@aws-cdk/aws-dynamodb:retainTableReplica": true, 91 | "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, 92 | "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, 93 | "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, 94 | "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, 95 | "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/lib/dynamodb-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 3 | 4 | export class DynamoDBStack extends cdk.Stack { 5 | public readonly usersTable: dynamodb.Table; 6 | constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { 7 | super(scope, id, props); 8 | 9 | this.usersTable = new dynamodb.Table(this, 'UsersTable', { 10 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 11 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 12 | removalPolicy: cdk.RemovalPolicy.DESTROY, 13 | tableName: `${this.stackName}-users-table`, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/lib/users-api-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 3 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 4 | import { Construct } from 'constructs'; 5 | import path from 'path'; 6 | import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2'; 7 | import * as apigateway_integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; 8 | import { DynamoDBStack } from './dynamodb-stack'; 9 | 10 | interface UsersApiStackProps extends cdk.StackProps { 11 | dynamodbStack: DynamoDBStack; 12 | } 13 | 14 | export class UsersApiStack extends cdk.Stack { 15 | constructor(scope: Construct, id: string, props: UsersApiStackProps) { 16 | super(scope, id, props); 17 | 18 | const userHandler = new NodejsFunction(this, 'UsersHandler', { 19 | runtime: Runtime.NODEJS_22_X, 20 | entry: path.join(__dirname, '../src/lambda/handler.ts'), 21 | handler: 'handler', 22 | functionName: `${this.stackName}-user-handler`, 23 | environment: { 24 | TABLE_NAME: props.dynamodbStack.usersTable.tableName, 25 | }, 26 | }); 27 | props.dynamodbStack.usersTable.grantReadWriteData(userHandler); 28 | 29 | const httpApi = new apigateway.HttpApi(this, 'UsersApi', { 30 | apiName: 'Users API', 31 | description: 'Users Management API', 32 | corsPreflight: { 33 | allowOrigins: ['*'], 34 | allowMethods: [apigateway.CorsHttpMethod.ANY], 35 | allowHeaders: ['*'], 36 | }, 37 | }); 38 | 39 | const routes = [ 40 | { path: '/users', method: apigateway.HttpMethod.GET, name: 'GetAllUsers' }, 41 | { path: '/users', method: apigateway.HttpMethod.POST, name: 'CreateUser' }, 42 | { path: '/users/{id}', method: apigateway.HttpMethod.GET, name: 'GetUser' }, 43 | { path: '/users/{id}', method: apigateway.HttpMethod.PUT, name: 'UpdateUser' }, 44 | { path: '/users/{id}', method: apigateway.HttpMethod.DELETE, name: 'DeleteUser' }, 45 | ]; 46 | routes.forEach(({ path, method, name }) => { 47 | httpApi.addRoutes({ 48 | path, 49 | methods: [method], 50 | integration: new apigateway_integrations.HttpLambdaIntegration(`${name}Integration`, userHandler), 51 | }); 52 | }); 53 | new cdk.CfnOutput(this, 'HttpApiUrl', { 54 | value: httpApi.url!, 55 | description: 'HTTP API URL', 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/makeRequests.http: -------------------------------------------------------------------------------- 1 | @URL = https://kyy7bbju0f.execute-api.eu-north-1.amazonaws.com 2 | 3 | 4 | ### Get all users 5 | GET {{URL}}/users 6 | 7 | ### Create a user 8 | POST {{URL}}/users 9 | Content-Type: application/json 10 | 11 | { 12 | "name": "coding addict", 13 | "email": "coding@addict.com" 14 | } 15 | 16 | ### Get a user 17 | GET {{URL}}/users/3f9a7857-7247-4b37-ac08-38f0299c5c36 18 | 19 | ### Update a user 20 | PUT {{URL}}/users/3f9a7857-7247-4b37-ac08-38f0299c5c36 21 | Content-Type: application/json 22 | 23 | { 24 | "name": "coding addict", 25 | "email": "coding@addict.com" 26 | } 27 | 28 | ### Delete a user 29 | DELETE {{URL}}/users/3f9a7857-7247-4b37-ac08-38f0299c5c36 30 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "users-api", 3 | "version": "0.1.0", 4 | "bin": { 5 | "users-api": "bin/users-api.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.14", 15 | "@types/node": "22.7.9", 16 | "aws-cdk": "2.1018.1", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.2.5", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.6.3" 21 | }, 22 | "dependencies": { 23 | "@aws-sdk/client-dynamodb": "^3.839.0", 24 | "@aws-sdk/lib-dynamodb": "^3.839.0", 25 | "@faker-js/faker": "^9.8.0", 26 | "@types/aws-lambda": "^8.10.150", 27 | "@types/uuid": "^10.0.0", 28 | "aws-cdk-lib": "2.200.1", 29 | "constructs": "^10.0.0", 30 | "esbuild": "^0.25.5", 31 | "uuid": "^11.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/src/lambda/handler.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda'; 2 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 3 | import { DynamoDBDocumentClient, PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import { faker } from '@faker-js/faker'; 6 | 7 | const client = new DynamoDBClient({}); 8 | const dynamoDB = DynamoDBDocumentClient.from(client); 9 | const TABLE_NAME = process.env.TABLE_NAME || ''; 10 | 11 | export const handler = async (event: APIGatewayProxyEventV2): Promise => { 12 | const method = event.requestContext.http.method; 13 | const path = event.requestContext.http.path; 14 | 15 | try { 16 | if (path === '/users') { 17 | switch (method) { 18 | case 'GET': 19 | return getAllUsers(event); 20 | case 'POST': 21 | return createUser(event); 22 | default: 23 | return { 24 | statusCode: 400, 25 | body: JSON.stringify({ message: 'Unsupported HTTP method for /users path ' }), 26 | }; 27 | } 28 | } 29 | 30 | if (path.startsWith('/users/')) { 31 | const userId = path.split('/users/')[1]; 32 | if (!userId) { 33 | return { 34 | statusCode: 400, 35 | body: JSON.stringify({ message: 'User ID is required' }), 36 | }; 37 | } 38 | switch (method) { 39 | case 'GET': 40 | return getUser(userId); 41 | case 'PUT': 42 | return updateUser(event, userId); 43 | case 'DELETE': 44 | return deleteUser(userId); 45 | default: 46 | return { 47 | statusCode: 400, 48 | body: JSON.stringify({ message: 'Unsupported HTTP method for users path ' }), 49 | }; 50 | } 51 | } 52 | return { 53 | statusCode: 404, 54 | body: JSON.stringify({ message: 'Not Found' }), 55 | }; 56 | } catch (error) { 57 | console.error('Error :', error); 58 | return { 59 | statusCode: 500, 60 | body: JSON.stringify({ message: 'Internal Server Error' }), 61 | }; 62 | } 63 | }; 64 | 65 | async function getAllUsers(event: APIGatewayProxyEventV2): Promise { 66 | const result = await dynamoDB.send( 67 | new ScanCommand({ 68 | TableName: TABLE_NAME, 69 | }) 70 | ); 71 | return { 72 | statusCode: 200, 73 | body: JSON.stringify(result.Items || []), 74 | }; 75 | } 76 | async function createUser(event: APIGatewayProxyEventV2): Promise { 77 | const { name, email } = JSON.parse(event.body!); 78 | const userId = uuidv4(); 79 | 80 | const user = { 81 | id: userId, 82 | name, 83 | email, 84 | createdAt: new Date().toISOString(), 85 | }; 86 | 87 | // const user = { 88 | // id: userId, 89 | // name: faker.person.fullName(), 90 | // email: faker.internet.email(), 91 | // createdAt: new Date().toISOString(), 92 | // }; 93 | 94 | await dynamoDB.send( 95 | new PutCommand({ 96 | TableName: TABLE_NAME, 97 | Item: user, 98 | }) 99 | ); 100 | return { 101 | statusCode: 201, 102 | body: JSON.stringify(user), 103 | }; 104 | } 105 | 106 | async function getUser(userId: string): Promise { 107 | const result = await dynamoDB.send( 108 | new GetCommand({ 109 | TableName: TABLE_NAME, 110 | Key: { id: userId }, 111 | }) 112 | ); 113 | if (!result.Item) { 114 | return { 115 | statusCode: 404, 116 | body: JSON.stringify({ message: 'User not found' }), 117 | }; 118 | } 119 | return { 120 | statusCode: 200, 121 | body: JSON.stringify(result.Item), 122 | }; 123 | } 124 | async function updateUser(event: APIGatewayProxyEventV2, userId: string): Promise { 125 | const { name, email } = JSON.parse(event.body!); 126 | 127 | const result = await dynamoDB.send( 128 | new UpdateCommand({ 129 | TableName: TABLE_NAME, 130 | Key: { id: userId }, 131 | UpdateExpression: 'SET #name = :name, #email = :email', 132 | ExpressionAttributeNames: { 133 | '#name': 'name', 134 | '#email': 'email', 135 | }, 136 | ExpressionAttributeValues: { 137 | ':name': name || null, 138 | ':email': email || null, 139 | }, 140 | ReturnValues: 'ALL_NEW', 141 | }) 142 | ); 143 | return { 144 | statusCode: 200, 145 | body: JSON.stringify(result.Attributes), 146 | }; 147 | } 148 | async function deleteUser(userId: string): Promise { 149 | await dynamoDB.send( 150 | new DeleteCommand({ 151 | TableName: TABLE_NAME, 152 | Key: { id: userId }, 153 | }) 154 | ); 155 | return { 156 | statusCode: 200, 157 | body: JSON.stringify({ message: `user deleted : ${userId}` }), 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/test/users-api.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as UsersApi from '../lib/users-api-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/users-api-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new UsersApi.UsersApiStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/aws-users-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "lib": [ 7 | "es2022" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-smilga/aws-cdk-course/ecbd59766a447a17f7821ac9dc834daf01646e8e/03-dynamodb-gateway/nextjs-frontend-app/app/favicon.ico -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.141 0.005 285.823); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.141 0.005 285.823); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.141 0.005 285.823); 54 | --primary: oklch(0.21 0.006 285.885); 55 | --primary-foreground: oklch(0.985 0 0); 56 | --secondary: oklch(0.967 0.001 286.375); 57 | --secondary-foreground: oklch(0.21 0.006 285.885); 58 | --muted: oklch(0.967 0.001 286.375); 59 | --muted-foreground: oklch(0.552 0.016 285.938); 60 | --accent: oklch(0.967 0.001 286.375); 61 | --accent-foreground: oklch(0.21 0.006 285.885); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.92 0.004 286.32); 64 | --input: oklch(0.92 0.004 286.32); 65 | --ring: oklch(0.705 0.015 286.067); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0 0); 72 | --sidebar-foreground: oklch(0.141 0.005 285.823); 73 | --sidebar-primary: oklch(0.21 0.006 285.885); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.967 0.001 286.375); 76 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 77 | --sidebar-border: oklch(0.92 0.004 286.32); 78 | --sidebar-ring: oklch(0.705 0.015 286.067); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.141 0.005 285.823); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.21 0.006 285.885); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.21 0.006 285.885); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.92 0.004 286.32); 89 | --primary-foreground: oklch(0.21 0.006 285.885); 90 | --secondary: oklch(0.274 0.006 286.033); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.274 0.006 286.033); 93 | --muted-foreground: oklch(0.705 0.015 286.067); 94 | --accent: oklch(0.274 0.006 286.033); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.552 0.016 285.938); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.21 0.006 285.885); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.274 0.006 286.033); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.552 0.016 285.938); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | import './globals.css'; 4 | import { ReactQueryProvider } from '@/components/ReactQueryProvider'; 5 | 6 | const geistSans = Geist({ 7 | variable: '--font-geist-sans', 8 | subsets: ['latin'], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: '--font-geist-mono', 13 | subsets: ['latin'], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Users App', 18 | description: 'Users App', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserCRUD } from '@/components/UserCRUD'; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/EditUserModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | import { Label } from '@/components/ui/label'; 8 | import { useUsers } from '@/hooks/useUsers'; 9 | import { userApi } from '@/lib/api'; 10 | import { CreateUserRequest } from '@/lib/types'; 11 | 12 | interface EditUserModalProps { 13 | userId: string | null; 14 | open: boolean; 15 | onClose: () => void; 16 | } 17 | 18 | export function EditUserModal({ userId, open, onClose }: EditUserModalProps) { 19 | const { updateUser, updateUserStatus } = useUsers(); 20 | const [formData, setFormData] = useState({ name: '', email: '' }); 21 | const [loading, setLoading] = useState(false); 22 | const [error, setError] = useState(null); 23 | 24 | useEffect(() => { 25 | if (userId && open) { 26 | setLoading(true); 27 | setError(null); 28 | userApi 29 | .getUserById(userId) 30 | .then((u) => { 31 | setFormData({ name: u.name, email: u.email }); 32 | }) 33 | .catch(() => setError('Failed to fetch user.')) 34 | .finally(() => setLoading(false)); 35 | } 36 | }, [userId, open]); 37 | 38 | const handleInputChange = (e: React.ChangeEvent) => { 39 | const { name, value } = e.target; 40 | setFormData((prev) => ({ ...prev, [name]: value })); 41 | }; 42 | 43 | const handleSubmit = async (e: React.FormEvent) => { 44 | e.preventDefault(); 45 | if (!userId) return; 46 | setError(null); 47 | try { 48 | await updateUser({ id: userId, data: formData }); 49 | onClose(); 50 | } catch (err) { 51 | console.error(err); 52 | setError('Failed to update user.'); 53 | } 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | Edit User 61 | Update the user information below. 62 | 63 | {loading ? ( 64 |
Loading...
65 | ) : error ? ( 66 |
{error}
67 | ) : ( 68 |
69 |
70 | 71 | 72 |
73 |
74 | 75 | 76 |
77 | 78 | 81 | 84 | 85 |
86 | )} 87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/ReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 5 | import { ReactNode, useState } from 'react'; 6 | 7 | export function ReactQueryProvider({ children }: { children: ReactNode }) { 8 | const [queryClient] = useState(() => new QueryClient()); 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/UserCRUD.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createContext, useContext, useState } from 'react'; 4 | import { UserForm } from './UserForm'; 5 | import { UserList } from './UserList'; 6 | 7 | // Create a context for sharing user state 8 | const UserContext = createContext<{ 9 | refreshUsers: () => void; 10 | } | null>(null); 11 | 12 | export const useUserContext = () => { 13 | const context = useContext(UserContext); 14 | if (!context) { 15 | throw new Error('useUserContext must be used within a UserProvider'); 16 | } 17 | return context; 18 | }; 19 | 20 | export function UserCRUD() { 21 | const [refreshKey, setRefreshKey] = useState(0); 22 | 23 | const refreshUsers = () => { 24 | setRefreshKey((prev) => prev + 1); 25 | }; 26 | 27 | return ( 28 | 29 |
30 |
31 |

User Management System

32 |

Create and manage users with our CRUD application

33 |
34 | 35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/UserForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { faker } from '@faker-js/faker'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Input } from '@/components/ui/input'; 7 | import { Label } from '@/components/ui/label'; 8 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 9 | import { useUsers } from '@/hooks/useUsers'; 10 | import { useUserContext } from './UserCRUD'; 11 | import { CreateUserRequest } from '@/lib/types'; 12 | 13 | export function UserForm() { 14 | const [formData, setFormData] = useState({ 15 | name: '', 16 | email: '', 17 | }); 18 | 19 | const { createUser, createUserStatus } = useUsers(); 20 | const { refreshUsers } = useUserContext(); 21 | 22 | const handleInputChange = (e: React.ChangeEvent) => { 23 | const { name, value } = e.target; 24 | setFormData((prev) => ({ 25 | ...prev, 26 | [name]: value, 27 | })); 28 | }; 29 | 30 | const generateRandomUser = () => { 31 | const randomName = faker.person.fullName(); 32 | const randomEmail = faker.internet.email({ firstName: randomName.split(' ')[0], lastName: randomName.split(' ')[1] }); 33 | 34 | setFormData({ 35 | name: randomName, 36 | email: randomEmail, 37 | }); 38 | }; 39 | 40 | const handleSubmit = async (e: React.FormEvent) => { 41 | e.preventDefault(); 42 | try { 43 | await createUser(formData); 44 | setFormData({ name: '', email: '' }); 45 | refreshUsers(); 46 | } catch (err) { 47 | console.error(err); 48 | // error handled by React Query 49 | } 50 | }; 51 | 52 | return ( 53 | 54 | 55 | Create New User 56 | Add a new user to the system 57 | 58 | 59 |
60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 |
69 | 70 |
71 | 74 | 77 |
78 | 79 | {createUserStatus === 'error' &&
Failed to create user. Please try again.
} 80 |
81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | showCloseButton = true, 53 | ...props 54 | }: React.ComponentProps & { 55 | showCloseButton?: boolean 56 | }) { 57 | return ( 58 | 59 | 60 | 68 | {children} 69 | {showCloseButton && ( 70 | 74 | 75 | Close 76 | 77 | )} 78 | 79 | 80 | ) 81 | } 82 | 83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 84 | return ( 85 |
90 | ) 91 | } 92 | 93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 94 | return ( 95 |
103 | ) 104 | } 105 | 106 | function DialogTitle({ 107 | className, 108 | ...props 109 | }: React.ComponentProps) { 110 | return ( 111 | 116 | ) 117 | } 118 | 119 | function DialogDescription({ 120 | className, 121 | ...props 122 | }: React.ComponentProps) { 123 | return ( 124 | 129 | ) 130 | } 131 | 132 | export { 133 | Dialog, 134 | DialogClose, 135 | DialogContent, 136 | DialogDescription, 137 | DialogFooter, 138 | DialogHeader, 139 | DialogOverlay, 140 | DialogPortal, 141 | DialogTitle, 142 | DialogTrigger, 143 | } 144 | -------------------------------------------------------------------------------- /03-dynamodb-gateway/nextjs-frontend-app/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | FormProvider, 9 | useFormContext, 10 | useFormState, 11 | type ControllerProps, 12 | type FieldPath, 13 | type FieldValues, 14 | } from "react-hook-form" 15 | 16 | import { cn } from "@/lib/utils" 17 | import { Label } from "@/components/ui/label" 18 | 19 | const Form = FormProvider 20 | 21 | type FormFieldContextValue< 22 | TFieldValues extends FieldValues = FieldValues, 23 | TName extends FieldPath = FieldPath, 24 | > = { 25 | name: TName 26 | } 27 | 28 | const FormFieldContext = React.createContext( 29 | {} as FormFieldContextValue 30 | ) 31 | 32 | const FormField = < 33 | TFieldValues extends FieldValues = FieldValues, 34 | TName extends FieldPath = FieldPath, 35 | >({ 36 | ...props 37 | }: ControllerProps) => { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const useFormField = () => { 46 | const fieldContext = React.useContext(FormFieldContext) 47 | const itemContext = React.useContext(FormItemContext) 48 | const { getFieldState } = useFormContext() 49 | const formState = useFormState({ name: fieldContext.name }) 50 | const fieldState = getFieldState(fieldContext.name, formState) 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within ") 54 | } 55 | 56 | const { id } = itemContext 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | } 66 | } 67 | 68 | type FormItemContextValue = { 69 | id: string 70 | } 71 | 72 | const FormItemContext = React.createContext( 73 | {} as FormItemContextValue 74 | ) 75 | 76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
86 | 87 | ) 88 | } 89 | 90 | function FormLabel({ 91 | className, 92 | ...props 93 | }: React.ComponentProps) { 94 | const { error, formItemId } = useFormField() 95 | 96 | return ( 97 |