diff --git a/.gitignore b/.gitignore index f2912a661..8c4a9a534 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ _site .jekyll-cache/ .DS_Store vendor/ +.idea/ diff --git a/Gemfile.lock b/Gemfile.lock index bb73cc321..caf647bd6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,6 +72,7 @@ GEM PLATFORMS arm64-darwin-21 + x86_64-darwin-22 DEPENDENCIES jekyll (= 4.1.1) @@ -81,7 +82,7 @@ DEPENDENCIES webrick (~> 1.7) RUBY VERSION - ruby 3.0.4p208 + ruby 2.6.10p210 BUNDLED WITH - 2.3.20 + 2.4.5 diff --git a/README.md b/README.md index e14207c66..80ea474eb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -

+

SST

-

+

Discourse posts Twitter follow Chat on Discord diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..8322ce7b8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,515 @@ +# Guide Typescript & PNPM Updates + +As I work through the guide, I add notes and I also read the comments and adding any relevant notes I see as of May 18(ish). + +## Site Wide + +#### Changes +- Added .idea to .gitignore +- Added .idea to Jekyll Exclude list + +#### Notes +- [ ] Consider accessibility features + * http://www.goring.org/resources/accessibility.html + * https://github.com/benbalter/ra11y + * adding an indicator for links that open in new tabs +- [ ] Consider adding "Archived" banner to the top of any parts of the site that are kept but are not intended for current reference for example, all the pages [in this section](http://localhost:4000/guide.html#archives). + +## Guide + +#### Changes +- Updated External Links to open a new tab. +- Updated SASS for blockquote within post-content to not be enormous. +- Added a few classes for Notes and Asides + +#### Notes +- [ ] TODO: Resolve SST Console Warning [See the migration guide](https://a.co/7PzMCcy) + + (node:85684) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023. + + Please migrate your code to use AWS SDK for JavaScript (v3). + For more information, check the migration guide at https://a.co/7PzMCcy + (Use `node --trace-warnings ...` to show where the warning was created) + 16:14:43.391 + +- [ ] TODO: Clean up legacy guide files so it doesn't clutter the current fileset. +- [ ] TODO: Correctly manage the path_id variable in code examples to ensure they are present without code duplication. +- [ ] TODO: Switch from Amplify API to RestApi or GraphQLAPI based on note in API.d.ts (See Warning in console.sst.dev) +- [ ] Consider adding a chapter on accessibility in the frontend creation section. + * [React Accessibility](https://legacy.reactjs.org/docs/accessibility.html) - New Accessibility guides are still pending based on announcement on 3/16/2023 + * https://www.freecodecamp.org/news/react-accessibility-tools-build-accessible-react-apps/ + * https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_accessibility + +## Chapter Specific + +### What Does This Guide Cover +#### Notes (From Discourse comments) +- [ ] [Consider using Next.js for the guide](https://discourse.sst.dev/t/what-does-this-guide-cover/83/7?u=christinep2) +- [ ] [Consider switching example to React Native](https://discourse.sst.dev/t/what-does-this-guide-cover/83/8?u=christinep2) + +### How to Get Help? +_no specific changes_ + +### What is Serverless? +#### Changes + - Removed link to the What is AWS Lambda chapter as it encourages skipping the rest of the current page. + +### What is AWS Lambda? +#### Notes +- [ ] [Consider decreasing specificity in Lambda Spec Description](/chapters/what-is-aws-lambda.html#lambda-specs) - As AWS modifies its offerings, the description can be outdated. By speaking in generalities and linking directly to AWS for specificity the document will be less fragile. + +### Why Create Serverless Apps? +#### Notes +- [ ] Consider specifying a timeframe on the cost calculation to account for pricing changes over time. + +### Create an AWS Account +#### Changes +- Tested steps with a new AWS Account. +- Removed reference to specific language since they changed the call to action. Also removed screenshot as it was outdated. + +### Create an IAM User +#### Changes +- Updated chapter instructions & screenshots. +- Updated screenshots at 1280x783. +- Screenshots located in `create-iam-user` +- Left old screenshots in `iam-user` for other languages as I couldn't update them to match the new instructions. + +### Configure the AWS CLI +#### Changes +- Tested steps with a clean install on Mac. +#### Notes +- [ ] Consider adding [Chocolatey](https://chocolatey.org/) [instructions for Windows](https://discourse.sst.dev/t/configure-the-aws-cli/86/3?u=christinep2. +- [ ] (Consider adding instructions for *NIX flavors.)[https://discourse.sst.dev/t/configure-the-aws-cli/86/5?u=christinep2] +- [ ] Consider adding a link to the AWS instructions instead, in case that is easier. + +### What is SST? +#### Changes +- Removed link to other chapters as it encourages skipping the current document +- Added links for the specific IDE Instructions +#### Notes +- [ ] Consider removing the penultimate sentence regarding the alternative guide since it is archived. + +### What is Infrastructure as Code? +#### Changes +- Added "using CDK" into the first sentence based on the confusion mentioned in the chapter comments + +### What is AWS CDK? +#### Changes +- Changed references to pnpm +- Added question mark to title to match the other titles in the section and the title of the comments page. + +### Create an SST app +#### Changes +- updated to pnpm + +#### Notes +- [ ] Need to update Next Steps command line output as follows + - cd notes + - pnpm add (or npm, or yarn) + - pnpm exec sst dev + +### Create a Hello World API +#### Changes +- updated to pnpm +- Added instructions for Safari et. al. users found in [this discord thread](https://discord.com/channels/983865673656705025/1102040862143303751/1102073623516282890) + +#### Notes +- [ ] Safari instructions for mkcert usage are not working. Need to investigate further. + +### Initialize a Github Repo +_no specific changes_ + +### Create a DynamoDB Table in SST +#### Changes +- updated to pnpm +- updated to typescript +- Added clarification as specified in comment + +#### Questions +1. Regarding the statement "There’s no specific reason why we are creating a separate stack for these resources." Would it be fair to say that the decision to have separate stacks increases application maintainability? It seems like it could fall under the principle of "separation of concerns". +2. Why is the Hello World API still present in console after this step? + +### Create a S3 Bucket in SST +#### Changes +- updated to pnpm +- updated to typescript +- small wording changes +- moved bucket in return to above table instead of below, matching location in the stack file and alphabetically ordered. +- git commit as a one line command to facilitate using the copy option for the code. + +### Review Our App Architecture +_no specific changes_ + +### Add an API to Create a Note +#### Changes +- updated to pnpm +- updated to typescript +- Added info about DRYness and maintainability to the refactor section. + +#### Notes +- Should we uninstall the aws-sdk (or @aws-sdk) from functions when we install it into core? +- Should the user re-test after the refactor to ensure it still works? +- Do we need to add [this note](https://discourse.sst.dev/t/add-an-api-to-create-a-note/2451/18?u=christinep2) for Windows users into the guide? +- Referenced [this error handling blog post](https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript) for the error code in handler. +- I'm seeing a delay as I add the API endpoints with seeing them in the console. I experienced on two different run-throughs, is this expected? If so, can we add a note to that effect at this point so people don't try and debug? + +### Add an API to Get/Put/Patch/Delete a Note +#### Changes +- updated to pnpm +- updated to typescript + +### Adding Auth to Our Serverless App +#### Changes +- Updated to typescript +- Updated to use docs variable instead of hardcoded url + +### Secure Our Serverless APIs +#### Changes +- Updated to typescript +- Switched to PNPM +- Used optional chaining operator for `requestContext.authorizer`. Should we have a different solution? + +### Setup a Stripe Account +#### Changes +- Redo screenshots and update text to match new UI + +### Handling Secrets in SST +#### Changes +- Rewrite to use the SST Secrets CLI based on the information found here: https://docs.sst.dev/config#should-i-use-configsecret-or-env-for-secrets + +### Add an API to Handle Billing +#### Changes +- Updated to typescript +- Switched to PNPM + +#### Notes +- [ ] Consider not using [nested ternary operators](https://medium.com/@benlmsc/stop-using-nested-ternary-operators-heres-why-53e7e078e65a). + +### Unit Tests in Serverless +#### Changes +- Switch to Typescript for testing +- Switch to PNPM +- Add instructions to add Vite Test to workspace + +#### Notes +- [ ] Consider including tests as we build out the API instead of at the end, or at least adding a note that this is a preferred approach for ensuring properly tested code. + +### Handle CORS in Serverless APIs +#### Changes +- Switch to Typescript +- Switch to PNPM + +### Handle CORS in S3 for File Uploads +#### Changes +- Switch to Typescript +- Switch to PNPM + +### Create a New React.js App +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +#### Notes +- [ ] Need to [replace create-react-app with one of the recommended solutions](https://github.com/facebook/create-react-app/issues/13072) as create-react-app is being deprecated. +- [ ] Consider adding tests alongside new code from this point forward. + +### Set up Custom Fonts +#### Changes +- Minor wording changes +- Update final screenshot to show tsx file instead of js file + +### Set up Bootstrap +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Handle Routes with React Router +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Create Containers +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Adding Links in the Navbar +#### Changes +- Switch to PNPM +- Switch to Typescript +- Minor wording changes + +### Handle 404s +#### Changes +- Switch to Typescript +- Minor wording changes + +### Configure AWS Amplify +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Create a Login Page +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Login with AWS Cognito +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Add the Session to the State +#### Changes +- Switch to Typescript + +### Load the State from the Session +#### Changes +- Switch to Typescript +- Move import of UseEffect above explainer so that consumer is not sidetracked by the import not being present. + +### Clear the Session on Logout +#### Changes +- Switch to Typescript + +### Redirect on Login and Logout +#### Changes +- Switch to Typescript + +### Give Feedback While Logging In +#### Changes +- Switch to Typescript + +### Create a Custom React Hook to Handle Form Fields +#### Changes +- Switch to Typescript + +### Create a Signup Page +No Changes + +### Create the Signup Form +#### Changes +- Switch to Typescript +- Minor wording changes + +### Signup with AWS Cognito +#### Changes +- Switch to Typescript +- Minor wording changes +### Notes +- [ ] Do we want to add the handling for UserExists in? https://discourse.sst.dev/t/signup-with-aws-cognito/130/74?u=christinep2 + +### Add the Create Note Page +#### Changes +- Switch to Typescript +- Minor wording changes + +### Call the Create API +#### Changes +- Switch to Typescript +- Minor wording changes + +### Upload a File to S3 +#### Changes +- Switch to Typescript + +### Upload a File to S3 +#### Changes +- Switch to Typescript +- Added helpful note from @sometimescasey + +#### Notes +- [ ] Update troubleshooting tips if outdated. + +### List All the Notes +#### Changes +- Switch to Typescript +- Minor wording changes + +### Call the List API +#### Changes +- Switch to Typescript +- Move Import immediately after adding code so consumer is not distracted by errors + +### Display a Note +#### Changes +- Switch to Typescript + +### Render the Note Form +#### Changes +- Switch to Typescript +- Minor wording changes +- Move Import immediately after adding code so consumer is not distracted by errors + +### Save Changes to a Note +#### Changes +- Switch to Typescript +- Minor wording changes + +### Delete a Note +#### Changes +- Switch to Typescript +- Minor wording changes + +### Create a Settings Page +#### Changes +- Switch to Typescript +- Minor wording changes + +### Add Stripe Keys to Config +#### Changes +- Switch to Typescript +- Switch to PNPM +- Minor wording changes + +### Create a Billing Form +#### Changes +- Switch to Typescript +- Switch to PNPM + +### Connect the Billing Form +#### Changes +- Switch to Typescript +- Switch to PNPM + +### Set up Secure Pages +No Changes + +### Create a Route That Redirects +#### Changes +- Switch to Typescript + +### Use the Redirect Routes +#### Changes +- Switch to Typescript + +### Redirect on Login +#### Changes +- Switch to Typescript + +### Purchase a Domain with Route 53 +Skipped. + +### Custom Domains in serverless APIs +#### Changes +- Switch to Typescript +- Minor wording changes + +### Custom Domains for React Apps on AWS +#### Changes +- Switch to Typescript/PNPM +- Minor wording changes + +#### Notes +- [ ] I tried switching the domain and the alias so that I could easily remove the domain alias and use it for my "real" app and leave the subdomain for this demo, but it erred with the message. + ``` + Error: Validation failed with the following errors: + [prod-notes-FrontendStack/ReactSite/Redirect/RedirectCertificate] DNS zone www-notes.manuals4life.com is not authoritative for certificate domain name manuals4life.com + ``` + In orde to keep the statement "You can switch these around so that the root domain redirects to the `www.` version as well.", I believe we'd need additional information dealing with the CAA record or whatever tripped it up. + +### Getting Production Ready +No Changes + +### Creating a CI/CD Pipeline for serverless +No Changes (other than the global new tab for external links) + +### Setting up Your Project on Seed +#### Changes +- Switch References to Typescript +- Minor wording changes +#### Notes +- [ ] Needs new Screenshots using serverless-stack demo notes app. +- [ ] Might add a screenshot pointing at settings in the UI? + +### Configure Secrets in Seed +#### Changes +- Switch References to sst secret instead of .env.local +- Minor wording changes + +### Deploying Through Seed +#### Changes +- minor wording changes + +### Debugging Full-Stack Serverless Apps +No Changes + +### Setup Error Reporting in React +#### Changes +- Switch to Typescript +- Switched to `pnpm add @sentry/react` from `@sentry/browser` +- Added an ErrorInfoType + +#### Notes +- [ ] Needs new Screenshots + +### Report API Errors in React +#### Changes +- Switch to Typescript + +### Setup an Error Boundary in React +#### Changes +- Switch to Typescript +- Change multiline ternary to if/else +- Updated screenshot for react error +#### Notes +- [ ] Needs new Screenshots for Sentry confirmations + +### Setup Error Logging in Serverless +#### Changes +- Switch to Typescript + +### Logic Errors in Lambda Functions +#### Changes +- Switch to Typescript +#### Notes +- [ ] Logging extra information is incorrectly displaying `[object Object]` + +### Unexpected Errors in Lambda Functions +#### Changes +- Switch references to Typescript + +### Errors Outside Lambda Functions +#### Changes +- Switch references to Typescript + +### Errors in API Gateway +#### Changes +- Switch references to Typescript + +### Wrapping Up +#### Changes +- Switch references to PNPM + + +## Additional Guide Information Pages still in use: + +### Auth In Serverless Apps +#### Changes +- Updated to typescript +- Updated referenced pages What is IAM and What is an ARN + +### What is IAM / What is an ARN +#### Changes +- Updated text to remove wording implying it is a step in the guide + +### Mapping Cognito Identity Id and User Pool Id + +### Cognito User Pool vs Identity Pool + +### Setting serverless environment variables in a React app +- Updated to pnpm +- Updated to typescript + + +## Outstanding Changes + +### Extra Credit series of chapters on user management +- [ ] TODO: Update these as they are referenced from "Give Feedback While Logging In" (Specifically links to http://localhost:4000/chapters/manage-user-accounts-in-aws-amplify.html) diff --git a/_chapters/add-an-api-to-create-a-note.md b/_chapters/add-an-api-to-create-a-note.md index 8e464f0bc..8d2678024 100644 --- a/_chapters/add-an-api-to-create-a-note.md +++ b/_chapters/add-an-api-to-create-a-note.md @@ -12,38 +12,38 @@ Let's get started by creating the API for our notes app. We'll first add an API to create a note. This API will take the note object as the input and store it in the database with a new id. The note object will contain the `content` field (the content of the note) and an `attachment` field (the URL to the uploaded file). -### Creating a Stack +### Creating the API Stack -{%change%} Create a new file in `stacks/ApiStack.js` and add the following. +{%change%} Create a new file in `stacks/ApiStack.ts` and add the following. -```js -import { Api, use } from "sst/constructs"; +```typescript +import { Api, StackContext, use } from "sst/constructs"; import { StorageStack } from "./StorageStack"; -export function ApiStack({ stack, app }) { - const { table } = use(StorageStack); - - // Create the API - const api = new Api(stack, "Api", { - defaults: { - function: { - bind: [table], - }, - }, - routes: { - "POST /notes": "packages/functions/src/create.main", - }, - }); - - // Show the API endpoint in the output - stack.addOutputs({ - ApiEndpoint: api.url, - }); - - // Return the API resource - return { - api, - }; +export function ApiStack({ stack }: StackContext) { + const { table } = use(StorageStack); + + // Create the API + const api = new Api(stack, "Api", { + defaults: { + function: { + bind: [table], + }, + }, + routes: { + "POST /notes": "packages/functions/src/create.main", + }, + }); + + // Show the API endpoint in the output + stack.addOutputs({ + ApiEndpoint: api.url, + }); + + // Return the API resource + return { + api, + }; } ``` @@ -53,9 +53,9 @@ We are doing a couple of things of note here. - This new `ApiStack` references the `table` resource from the `StorageStack` that we created previously. -- We are creating an API using SST's [`Api`]({{ site.docs_url }}/constructs/Api) construct. +- We are creating an API using SST's [`Api`]({{ site.docs_url }}/constructs/Api){:target="_blank"} construct. -- We are [binding]({{ site.docs_url }}/resource-binding) our DynamoDB table to our API using the `bind` prop. This will allow our API to access our table. +- We are [binding]({{ site.docs_url }}/resource-binding){:target="_blank"} our DynamoDB table to our API using the `bind` prop. This will allow our API to access our table. - The first route we are adding to our API is the `POST /notes` route. It'll be used to create a note. @@ -65,48 +65,58 @@ We are doing a couple of things of note here. Let's add this new stack to the rest of our app. -{%change%} In `sst.config.ts`, import the API stack at the top. +{%change%} In `sst.config.ts`, replace the `stacks` function with - -```js -import { ApiStack } from "./stacks/ApiStack"; -``` - -{%change%} And, replace the `stacks` function with - - -```js +```typescript stacks(app) { app.stack(StorageStack).stack(ApiStack); }, ``` +{%change%} And, import the API stack at the top. +```typescript +import { ApiStack } from "./stacks/ApiStack"; +``` + + ### Add the Function Now let's add the function that'll be creating our note. -{%change%} Create a new file in `packages/functions/src/create.js` with the following. +{%change%} Create a new file in `packages/functions/src/create.ts` with the following. -```js -import * as uuid from "uuid"; +```typescript +import { APIGatewayProxyEvent } from "aws-lambda"; import AWS from "aws-sdk"; +import * as uuid from "uuid"; + import { Table } from "sst/node/table"; const dynamoDb = new AWS.DynamoDB.DocumentClient(); -export async function main(event) { +export async function main(event: APIGatewayProxyEvent) { + let data, params; + // Request body is passed in as a JSON encoded string in 'event.body' - const data = JSON.parse(event.body); - - const params = { - TableName: Table.Notes.tableName, - Item: { - // The attributes of the item to be created - userId: "123", // The id of the author - noteId: uuid.v1(), // A unique uuid - content: data.content, // Parsed from request body - attachment: data.attachment, // Parsed from request body - createdAt: Date.now(), // Current Unix timestamp - }, - }; + if (event.body) { + data = JSON.parse(event.body); + params = { + TableName: Table.Notes.tableName, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + } else { + return { + statusCode: 404, + body: JSON.stringify({ error: true }), + }; + } try { await dynamoDb.put(params).promise(); @@ -115,13 +125,20 @@ export async function main(event) { statusCode: 200, body: JSON.stringify(params.Item), }; - } catch (e) { + } catch (error) { + let message; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } return { statusCode: 500, - body: JSON.stringify({ error: e.message }), + body: JSON.stringify({ error: message }), }; } } + ``` There are some helpful comments in the code but let's go over them quickly. @@ -129,29 +146,31 @@ There are some helpful comments in the code but let's go over them quickly. - Parse the input from the `event.body`. This represents the HTTP request body. - It contains the contents of the note, as a string — `content`. - It also contains an `attachment`, if one exists. It's the filename of a file that will be uploaded to [our S3 bucket]({% link _chapters/create-an-s3-bucket-in-sst.md %}). -- We can access our DynamoDB table through `Table.Notes.tableName` from the `sst/node/table`, the [SST Node.js client]({{ site.docs_url }}/clients). Here `Notes` in `Table.Notes` is the name of our Table construct from the [Create a DynamoDB Table in SST]({% link _chapters/create-a-dynamodb-table-in-sst.md %}) chapter. By doing `bind: [table]` earlier in this chapter, we are allowing our API to access our table. +- We can access our DynamoDB table through `Table.Notes.tableName` from the `sst/node/table`, the [SST Node.js client]({{ site.docs_url }}/clients){:target="_blank"}. Here `Notes` in `Table.Notes` is the name of our Table construct from the [Create a DynamoDB Table in SST]({% link _chapters/create-a-dynamodb-table-in-sst.md %}) chapter. By doing `bind: [table]` earlier in this chapter, we are allowing our API to access our table. - The `userId` is the id for the author of the note. For now we are hardcoding it to `123`. Later we'll be setting this based on the authenticated user. - Make a call to DynamoDB to put a new object with a generated `noteId` and the current date as the `createdAt`. - And if the DynamoDB call fails then return an error with the HTTP status code `500`. -Let's go ahead and install the npm packages that we are using here. +Let's go ahead and install the packages that we are using here. -{%change%} Run the following in the `packages/functions/` folder. +{%change%} Navigate to the `functions` folder in your terminal. ```bash -$ npm install aws-sdk uuid +$ cd packages/functions ``` -- **aws-sdk** allows us to talk to the various AWS services. -- **uuid** generates unique ids. +{%change%} Then, run the following **in the `packages/functions/` folder** (Not in root). -### Deploy Our Changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. +```bash +$ pnpm add --save aws-sdk aws-lambda uuid;pnpm add --save-dev @types/uuid @types/aws-lambda +``` -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. +- **aws-sdk** allows us to talk to the various AWS services. +- **aws-lambda** +- **uuid** generates unique ids. +- **@types/uuid** provides the TypeScript types for `uuid`. -You should see that the new API stack has been deployed. +{%deploy%} ```bash ✓ Deployed: @@ -172,7 +191,7 @@ Head over to the **API** tab in the [SST Console]({{ site.old_console_url }}) an Here we can test our APIs. -{%change%} Add the following request body to the **Body** field and hit **Send**. +{%change%} Switch to the body tab in the API and add the following request body to the **Body** field and hit **Send**. ```json {"content":"Hello World","attachment":"hello.jpg"} @@ -192,36 +211,48 @@ Make a note of the `noteId`. We are going to use this newly created note in the ### Refactor Our Code -Before we move on to the next chapter, let's quickly refactor the code since we are going to be doing much of the same for all of our APIs. +Before we move on to the next chapter, let's refactor this code. Since we'll be doing the same basic actions for all of our APIs, it makes sense to [DRY our code](https://blog.boot.dev/clean-code/dry-code/){:target="_blank"} to create reusable shared behaviors for both application reliability and maintainability. -{%change%} Start by replacing our `create.js` with the following. +{%change%} Start by replacing our `create.ts` with the following. -```js +```typescript +import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; import { Table } from "sst/node/table"; import * as uuid from "uuid"; -import handler from "@notes/core/handler"; import dynamoDb from "@notes/core/dynamodb"; -export const main = handler(async (event) => { - const data = JSON.parse(event.body); - const params = { - TableName: Table.Notes.tableName, - Item: { - // The attributes of the item to be created - userId: "123", // The id of the author - noteId: uuid.v1(), // A unique uuid - content: data.content, // Parsed from request body - attachment: data.attachment, // Parsed from request body - createdAt: Date.now(), // Current Unix timestamp - }, - }; +export const main = handler(async (event: APIGatewayProxyEvent) => { + let data = { + content: '', + attachment: '' + } - await dynamoDb.put(params); + if (event.body != null) { + data = JSON.parse(event.body); + } - return params.Item; + const params = { + TableName: Table.Notes.tableName, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + + + await dynamoDb.put(params); + + return params.Item; }); ``` +{%change%} Run the following in the `packages/functions/` folder (Not in root). + This code doesn't work just yet but it shows you what we want to accomplish: - We want to make our Lambda function `async`, and simply return the results. @@ -231,39 +262,51 @@ This code doesn't work just yet but it shows you what we want to accomplish: Let's start by creating a `dynamodb` util that we can share across all our functions. We'll place this in the `packages/core` directory. This is where we'll be putting all our business logic. -{%change%} Create a `packages/core/src/dynamodb.js` file with: +{%change%} Create a `packages/core/src/dynamodb.ts` file with: -```js +```typescript import AWS from "aws-sdk"; +import {DocumentClient} from "aws-sdk/lib/dynamodb/document_client"; const client = new AWS.DynamoDB.DocumentClient(); export default { - get: (params) => client.get(params).promise(), - put: (params) => client.put(params).promise(), - query: (params) => client.query(params).promise(), - update: (params) => client.update(params).promise(), - delete: (params) => client.delete(params).promise(), + get: (params: DocumentClient.GetItemInput) => client.get(params).promise(), + put: (params: DocumentClient.PutItemInput) => client.put(params).promise(), + query: (params: DocumentClient.QueryInput) => client.query(params).promise(), + update: (params: DocumentClient.UpdateItemInput) => client.update(params).promise(), + delete: (params: DocumentClient.DeleteItemInput) => client.delete(params).promise(), }; + +``` + +Here we are creating a convenience object that exposes the DynamoDB client methods that we are going to need in this guide. We are now using the aws-sdk to `core` as well. Run the following **in the packages/core/ folder**. + + +```bash +$ pnpm add --save aws-sdk aws-lambda;pnpm add --save-dev @types/aws-lambda ``` -Here we are creating a convenience object that exposes the DynamoDB client methods that we are going to need in this guide. +{%change%} Also create a `packages/core/src/handler.ts` file with the following. -{%change%} Also create a `packages/core/src/handler.js` file with the following. +```typescript +import { Context, APIGatewayEvent } from 'aws-lambda'; -```js -export default function handler(lambda) { - return async function (event, context) { +export default function handler(lambda: Function) { + return async function (event: APIGatewayEvent, context: Context) { let body, statusCode; try { // Run the Lambda body = await lambda(event, context); statusCode = 200; - } catch (e) { - console.error(e); - body = { error: e.message }; + } catch (error) { statusCode = 500; + if (error instanceof Error) { + body = { error: error.message }; + } else { + body = { error: String(error) }; + } } // Return HTTP response @@ -283,7 +326,7 @@ Let's go over this in detail. - On success, we `JSON.stringify` the result and return it with a `200` status code. - If there is an error then we return the error message with a `500` status code. -It's **important to note** that the `handler.js` needs to be **imported before we import anything else**. This is because we'll be adding some error handling to it later that needs to be initialized when our Lambda function is first invoked. +{%handler_caveat%} Next, we are going to add the API to get a note given its id. @@ -291,11 +334,15 @@ Next, we are going to add the API to get a note given its id. #### Common Issues +- path received type undefined + + Restarting `pnpm exec sst dev` should pick up the new type information and resolve this error. + - Response `statusCode: 500` - If you see a `statusCode: 500` response when you invoke your function, the error has been reported by our code in the `catch` block. You'll see a `console.error` is included in our `handler.js` code above. Incorporating logs like these can help give you insight on issues and how to resolve them. + If you see a `statusCode: 500` response when you invoke your function, the error has been reported by our code in the `catch` block. You'll see a `console.error` is included in our `handler.ts` code above. Incorporating logs like these can help give you insight on issues and how to resolve them. - ```js + ```typescript } catch (e) { // Prints the full error console.error(e); diff --git a/_chapters/add-an-api-to-delete-a-note.md b/_chapters/add-an-api-to-delete-a-note.md index ecedcc675..118eec90e 100644 --- a/_chapters/add-an-api-to-delete-a-note.md +++ b/_chapters/add-an-api-to-delete-a-note.md @@ -12,26 +12,34 @@ Finally, we are going to create an API that allows a user to delete a given note ### Add the Function -{%change%} Create a new file in `packages/functions/src/delete.js` and paste the following. +{%change%} Create a new file in `packages/functions/src/delete.ts` and paste the following. -```js -import { Table } from "sst/node/table"; +```typescript import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; import dynamoDb from "@notes/core/dynamodb"; -export const main = handler(async (event) => { - const params = { - TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be removed - Key: { - userId: "123", // The id of the author - noteId: event.pathParameters.id, // The id of the note from the path - }, - }; +export const main = handler(async (event: APIGatewayProxyEvent) => { + + let path_id + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + Key: { + userId: "123", // The id of the author + noteId: path_id, // The id of the note from the path + }, + }; - await dynamoDb.delete(params); + await dynamoDb.delete(params); - return { status: true }; + return { status: true }; }); ``` @@ -41,19 +49,13 @@ This makes a DynamoDB `delete` call with the `userId` & `noteId` key to delete t Let's add a new route for the delete note API. -{%change%} Add the following below the `PUT /notes{id}` route in `stacks/ApiStack.js`. +{%change%} Add the following below the `PUT /notes{id}` route in `stacks/ApiStack.ts`. -```js +```typescript "DELETE /notes/{id}": "packages/functions/src/delete.main", ``` -### Deploy Our Changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see that the API stack is being updated. +{%deploy%} ```bash ✓ Deployed: diff --git a/_chapters/add-an-api-to-get-a-note.md b/_chapters/add-an-api-to-get-a-note.md index 406e07162..6ebe79485 100644 --- a/_chapters/add-an-api-to-get-a-note.md +++ b/_chapters/add-an-api-to-get-a-note.md @@ -12,52 +12,56 @@ Now that we [created a note]({% link _chapters/add-an-api-to-create-a-note.md %} ### Add the Function -{%change%} Create a new file in `packages/functions/src/get.js` in your project root with the following: +{%change%} Create a new file in `packages/functions/src/get.ts` in your project root with the following: -```js -import { Table } from "sst/node/table"; +```typescript import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; import dynamoDb from "@notes/core/dynamodb"; -export const main = handler(async (event) => { - const params = { - TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved - Key: { - userId: "123", // The id of the author - noteId: event.pathParameters.id, // The id of the note from the path - }, - }; - - const result = await dynamoDb.get(params); - if (!result.Item) { - throw new Error("Item not found."); - } - - // Return the retrieved item - return result.Item; +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + const params = { + TableName: Table.Notes.tableName, + // 'Key' defines the partition key and sort key of + // the item to be retrieved + Key: { + userId: "123", // The id of the author + noteId: path_id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; }); ``` -This follows exactly the same structure as our previous `create.js` function. The major difference here is that we are doing a `dynamoDb.get(params)` to get a note object given the `userId` (still hardcoded) and `noteId` that's passed in through the request. +This follows exactly the same structure as our previous `create.ts` function. The major difference here is that we are doing a `dynamoDb.get(params)` to get a note object given the `userId` (still hardcoded) and `noteId` that's passed in through the request. ### Add the route Let's add a new route for the get note API. -{%change%} Add the following below the `POST /notes` route in `stacks/ApiStack.js`. +{%change%} Add the following above the `POST /notes` route in `stacks/ApiStack.ts`. -```js +```typescript "GET /notes/{id}": "packages/functions/src/get.main", ``` -### Deploy our changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see that the API stack is being updated. +{%deploy%} ```bash ✓ Deployed: @@ -72,9 +76,9 @@ Let's test the get notes API. In the [previous chapter]({% link _chapters/add-an Head back to the **API** tab in the [SST Console]({{ site.old_console_url }}) and select the `/notes/{id}` API. You might have to refresh your console. -{%change%} Set the `noteId` as the **id** and click **Send**. +{%change%} Select the URL tab and enter the uuid for the previously created note in the value field for `id`. Then click the **Send** button. -You should see the note being returned in the response. +You should see the note returned in the response. ![SST Console get note API request](/assets/part2/sst-console-get-note-api-request.png) diff --git a/_chapters/add-an-api-to-handle-billing.md b/_chapters/add-an-api-to-handle-billing.md index 2b151510d..aaf927114 100644 --- a/_chapters/add-an-api-to-handle-billing.md +++ b/_chapters/add-an-api-to-handle-billing.md @@ -12,35 +12,53 @@ Now let's get started with creating an API to handle billing. It's going to take ### Add a Billing Lambda -{%change%} Start by installing the Stripe NPM package. Run the following in the `packages/functions/` folder of our project. +{%change%} Start by installing the Stripe NPM package. Run the following **in the `packages/functions/` folder** of our project. ```bash -$ npm install stripe +$ pnpm add --save stripe ``` -{%change%} Create a new file in `packages/functions/src/billing.js` with the following. +{%change%} Create a new file in `packages/functions/src/billing.ts` with the following. -```js -import Stripe from "stripe"; +```typescript import handler from "@notes/core/handler"; -import { calculateCost } from "@notes/core/cost"; - -export const main = handler(async (event) => { - const { storage, source } = JSON.parse(event.body); - const amount = calculateCost(storage); - const description = "Scratch charge"; - - // Load our secret key from the environment variables - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - - await stripe.charges.create({ - source, - amount, - description, - currency: "usd", - }); - - return { status: true }; +import Stripe from "stripe"; +import {calculateCost} from "@notes/core/cost"; +import {APIGatewayProxyEvent} from "aws-lambda"; +import {Config} from "sst/node/config"; + +export const main = handler(async (event: APIGatewayProxyEvent) => { + let data = { + storage: 0, + source: null + }; + + if (event.body != null) { + data = JSON.parse(event.body); + } + + const { storage, source } = data; + + if (storage === 0 || source === null) { + throw new Error("Please provide valid transaction details."); + } + + const amount: number = calculateCost(storage); + const description: string = "Scratch charge"; + + // Load our secret key + const stripe = new Stripe(Config.STRIPE_SECRET_KEY,{ + apiVersion: '2022-11-15', + }); + + await stripe.charges.create({ + source, + amount, + description, + currency: "usd", + }); + + return { status: true }; }); ``` @@ -50,20 +68,20 @@ Most of this is fairly straightforward but let's go over it quickly: - We are using a `calculateCost(storage)` function (that we are going to add soon) to figure out how much to charge a user based on the number of notes that are going to be stored. -- We create a new Stripe object using our Stripe Secret key. We are getting this from the environment variable that we configured in the [previous chapter]({% link _chapters/handling-secrets-in-sst.md %}). For newer versions of the Stripe SDK, you might've to pass in an API version. +- We create a new Stripe object using our Stripe Secret key. We are getting this from the environment variable that we configured in the [previous chapter]({% link _chapters/handling-secrets-in-sst.md %}). At the time of this guide's writing, we are using apiVersion `2022-11-15` but you can check the [Stripe documentation](https://stripe.com/docs/api/versioning){:target="_blank"} for the latest version. - Finally, we use the `stripe.charges.create` method to charge the user and respond to the request if everything went through successfully. -Note, if you are testing this from India, you'll need to add some shipping information as well. Check out the [details from our forums](https://discourse.sst.dev/t/test-the-billing-api/172/20). +Note, if you are testing this from India, you'll need to add some shipping information as well. Check out the [details from our forums](https://discourse.sst.dev/t/test-the-billing-api/172/20){:target="_blank"}. ### Add the Business Logic Now let's implement our `calculateCost` method. This is primarily our _business logic_. -{%change%} Create a `packages/core/src/cost.js` and add the following. +{%change%} Create a `packages/core/src/cost.ts` and add the following. -```js -export function calculateCost(storage) { +```typescript +export function calculateCost(storage: number) { const rate = storage <= 10 ? 4 : storage <= 100 ? 2 : 1; return rate * storage * 100; } @@ -77,19 +95,13 @@ Clearly, our serverless infrastructure might be cheap but our service isn't! Let's add a new route for our billing API. -{%change%} Add the following below the `DELETE /notes/{id}` route in `stacks/ApiStack.js`. +{%change%} Add the following below the `DELETE /notes/{id}` route in `stacks/ApiStack.ts`. -```js +```typescript "POST /billing": "packages/functions/src/billing.main", ``` -### Deploy Our Changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see that the API stack is being updated. +{%deploy%} ```bash ✓ Deployed: @@ -102,28 +114,31 @@ You should see that the API stack is being updated. Now that we have our billing API all set up, let's do a quick test in our local environment. -We'll be using the same CLI from [a few chapters ago]({% link _chapters/secure-our-serverless-apis.md %}). +We'll be using the same CLI from [a few chapters ago]({% link _chapters/secure-our-serverless-apis.md %}){:target="_blank"}. -{%change%} Run the following in your terminal. +{%change%} Run the following in your terminal (If you have installed the tool you can use `apig-test` in place of `pnpm dlx` ). ```bash -$ npx aws-api-gateway-cli-test \ +$ pnpm dlx aws-api-gateway-cli-test \ --username='admin@example.com' \ --password='Passw0rd!' \ ---user-pool-id='USER_POOL_ID' \ ---app-client-id='USER_POOL_CLIENT_ID' \ ---cognito-region='COGNITO_REGION' \ ---identity-pool-id='IDENTITY_POOL_ID' \ ---invoke-url='API_ENDPOINT' \ ---api-gateway-region='API_REGION' \ +--user-pool-id='' \ +--app-client-id='' \ +--cognito-region='' \ +--identity-pool-id='' \ +--invoke-url='' \ +--api-gateway-region='' \ --path-template='/billing' \ --method='POST' \ --body='{"source":"tok_visa","storage":21}' ``` +{%note%} +Make sure to replace the `USER_POOL_ID`, `USER_POOL_CLIENT_ID`, `COGNITO_REGION`, `IDENTITY_POOL_ID`, `API_ENDPOINT`, and `API_REGION` with the [same values we used a couple of chapters ago]({% link _chapters/secure-our-serverless-apis.md %}){:target="_blank"}. -Make sure to replace the `USER_POOL_ID`, `USER_POOL_CLIENT_ID`, `COGNITO_REGION`, `IDENTITY_POOL_ID`, `API_ENDPOINT`, and `API_REGION` with the [same values we used a couple of chapters ago]({% link _chapters/secure-our-serverless-apis.md %}). +If you have the previous request, update the `path-template` and `body` with the new values. +{%endnote%} -Here we are testing with a Stripe test token called `tok_visa` and with `21` as the number of notes we want to store. You can read more about the Stripe test cards and tokens in the [Stripe API Docs here](https://stripe.com/docs/testing#cards). +Here we are testing with a Stripe test token called `tok_visa` and with `21` as the number of notes we want to store. You can read more about the Stripe test cards and tokens in the [Stripe API Docs here](https://stripe.com/docs/testing#cards){:target="_blank"}. If the command is successful, the response will look similar to this. diff --git a/_chapters/add-an-api-to-list-all-the-notes.md b/_chapters/add-an-api-to-list-all-the-notes.md index 539a7f2c8..2d34aa1ad 100644 --- a/_chapters/add-an-api-to-list-all-the-notes.md +++ b/_chapters/add-an-api-to-list-all-the-notes.md @@ -12,53 +12,48 @@ Now we are going to add an API that returns a list of all the notes a user has. ### Add the Function -{%change%} Create a new file in `packages/functions/src/list.js` with the following. +{%change%} Create a new file in `packages/functions/src/list.ts` with the following. -```js -import { Table } from "sst/node/table"; +```typescript import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; import dynamoDb from "@notes/core/dynamodb"; export const main = handler(async () => { - const params = { - TableName: Table.Notes.tableName, - // 'KeyConditionExpression' defines the condition for the query - // - 'userId = :userId': only return items with matching 'userId' - // partition key - KeyConditionExpression: "userId = :userId", - // 'ExpressionAttributeValues' defines the value in the condition - // - ':userId': defines 'userId' to be the id of the author - ExpressionAttributeValues: { - ":userId": "123", - }, - }; - - const result = await dynamoDb.query(params); - - // Return the matching list of items in response body - return result.Items; + const params = { + TableName: Table.Notes.tableName, + // 'KeyConditionExpression' defines the condition for the query + // - 'userId = :userId': only return items with matching 'userId' + // partition key + KeyConditionExpression: "userId = :userId", + // 'ExpressionAttributeValues' defines the value in the condition + // - ':userId': defines 'userId' to be the id of the author + ExpressionAttributeValues: { + ":userId": "123", + }, + }; + + const result = await dynamoDb.query(params); + + // Return the matching list of items in response body + return result.Items; }); ``` -This is pretty much the same as our `get.js` except we use a condition to only return the items that have the same `userId` as the one we are passing in. In our case, it's still hardcoded to `123`. +This is pretty much the same as our `get.ts` except we use a condition to only return the items that have the same `userId` as the one we are passing in. In our case, it's still hardcoded to `123`. ### Add the Route Let's add the route for this new endpoint. -{%change%} Add the following above the `POST /notes` route in `stacks/ApiStack.js`. +{%change%} Add the following above the `POST /notes` route in `stacks/ApiStack.ts`. -```js +```typescript "GET /notes": "packages/functions/src/list.main", ``` -### Deploy Our Changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see that the API stack is being updated. +{%deploy%} ```bash ✓ Deployed: @@ -77,6 +72,6 @@ You should see the notes being returned in the response. ![SST Console list notes API request](/assets/part2/sst-console-list-notes-api-request.png) -Note that, we are getting an array of notes. Instead of a single note. +Notice that we are getting an array of notes. Instead of a single note. Next we are going to add an API to update a note. diff --git a/_chapters/add-an-api-to-update-a-note.md b/_chapters/add-an-api-to-update-a-note.md index 00f49f94e..47ad9bbc2 100644 --- a/_chapters/add-an-api-to-update-a-note.md +++ b/_chapters/add-an-api-to-update-a-note.md @@ -12,60 +12,71 @@ Now let's create an API that allows a user to update a note with a new note obje ### Add the Function -{%change%} Create a new file in `packages/functions/src/update.js` and paste the following. +{%change%} Create a new file in `packages/functions/src/update.ts` and paste the following. -```js -import { Table } from "sst/node/table"; +```typescript import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; +import * as uuid from "uuid"; import dynamoDb from "@notes/core/dynamodb"; -export const main = handler(async (event) => { - const data = JSON.parse(event.body); - const params = { - TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be updated - Key: { - userId: "123", // The id of the author - noteId: event.pathParameters.id, // The id of the note from the path - }, - // 'UpdateExpression' defines the attributes to be updated - // 'ExpressionAttributeValues' defines the value in the update expression - UpdateExpression: "SET content = :content, attachment = :attachment", - ExpressionAttributeValues: { - ":attachment": data.attachment || null, - ":content": data.content || null, - }, - // 'ReturnValues' specifies if and how to return the item's attributes, - // where ALL_NEW returns all attributes of the item after the update; you - // can inspect 'result' below to see how it works with different settings - ReturnValues: "ALL_NEW", - }; - - await dynamoDb.update(params); - - return { status: true }; +export const main = handler(async (event: APIGatewayProxyEvent) => { + let data = { + content: '', + attachment: '' + } + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + + if (event.body != null) { + data = JSON.parse(event.body); + } + + const params = { + TableName: Table.Notes.tableName, + Key: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: path_id, // The id of the note from the path + }, + // 'UpdateExpression' defines the attributes to be updated + // 'ExpressionAttributeValues' defines the value in the update expression + UpdateExpression: "SET content = :content, attachment = :attachment", + ExpressionAttributeValues: { + ":attachment": data.attachment || null, + ":content": data.content || null, + }, + // 'ReturnValues' specifies if and how to return the item's attributes, + // where ALL_NEW returns all attributes of the item after the update; you + // can inspect 'result' below to see how it works with different settings + ReturnValues: "ALL_NEW", + }; + + await dynamoDb.update(params); + + return { status: true }; }); ``` -This should look similar to the `create.js` function. Here we make an `update` DynamoDB call with the new `content` and `attachment` values in the `params`. +This should look similar to the `create.ts` function combined with the validation from `get.ts` . Here we make an `update` DynamoDB call with the new `content` and `attachment` values in the `params`. ### Add the Route Let's add a new route for the get note API. -{%change%} Add the following below the `GET /notes/{id}` route in `stacks/ApiStack.js`. +{%change%} Add the following below the `GET /notes/{id}` route in `stacks/ApiStack.ts`. -```js +```typescript "PUT /notes/{id}": "packages/functions/src/update.main", ``` -### Deploy Our Changes - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see that the API stack is being updated. +{%deploy%} ```bash ✓ Deployed: @@ -78,7 +89,7 @@ You should see that the API stack is being updated. Now we are ready to test the new API. In [an earlier chapter]({% link _chapters/add-an-api-to-get-a-note.md %}) we tested our create note API. It should've returned the new note's id as the `noteId`. -Head to the **API** tab in the [SST Console]({{ site.old_console_url }}) and select the `PUT /notes/{id}` API. +Head to the **API** tab in the [SST Console]({{ site.old_console_url }}){:target="_blank"} and select the `PUT /notes/{id}` API. {%change%} Set the `noteId` as the **id** and in the **Body** tab set the following as the request body. Then hit **Send**. diff --git a/_chapters/add-app-favicons.md b/_chapters/add-app-favicons.md index eacf87909..c13991269 100644 --- a/_chapters/add-app-favicons.md +++ b/_chapters/add-app-favicons.md @@ -12,11 +12,11 @@ Create React App generates a simple favicon for our app and places it in `public For our example, we are going to start with a simple image and generate the various versions from it. -**Right-click to download** the following image. Or head over to this link to download it — [{{ '/assets/scratch-icon.png' | absolute_url }}]({{ '/assets/scratch-icon.png' | absolute_url }}) +**Right-click to download** the following image. Or head over to this link to download it — [{{ '/assets/scratch-icon.png' | absolute_url }}]({{ '/assets/scratch-icon.png' | absolute_url }}){:target="_blank"} App Icon -To ensure that our icon works for most of our targeted platforms we'll use a service called the [**Favicon Generator**](http://realfavicongenerator.net). +To ensure that our icon works for most of our targeted platforms we'll use a service called the [**Favicon Generator**](http://realfavicongenerator.net){:target="_blank"}. Click **Select your Favicon picture** to upload our icon. @@ -34,7 +34,9 @@ This should generate your favicon package and the accompanying code. Let's remove the old icons files. -**Note that, moving forward we'll be working exclusively in the `frontend/` directory.** +{%note%} +We'll be working exclusively in the `frontend/` directory through the chapter on securing React pages.** +{%endnote%} {%change%} Run the following from our `frontend/` directory. @@ -69,7 +71,7 @@ $ rm public/logo192.png public/logo512.png public/favicon.ico To include a file from the `public/` directory in your HTML, Create React App needs the `%PUBLIC_URL%` prefix. -{%change%} Add this to your `public/index.html`. +{%change%} Add this to the `` in your `public/index.html`. ```html - - - + + + + + + ``` -Finally head over to your browser and try the `/favicon-32x32.png` path to ensure that the files were added correctly. +Finally head over to your browser and add `/favicon-32x32.png` to the base URL path to ensure that the files were added correctly. Next we are going to look into setting up custom fonts in our app. diff --git a/_chapters/add-stripe-keys-to-config.md b/_chapters/add-stripe-keys-to-config.md index e59424d86..bd54c0e13 100644 --- a/_chapters/add-stripe-keys-to-config.md +++ b/_chapters/add-stripe-keys-to-config.md @@ -10,35 +10,35 @@ comments_id: add-stripe-keys-to-config/185 Back in the [Setup a Stripe account]({% link _chapters/setup-a-stripe-account.md %}) chapter, we had two keys in the Stripe console. The **Secret key** that we used in the backend and the **Publishable key**. The **Publishable key** is meant to be used in the frontend. -{%change%} Add the following line below the `const config = {` line in your `src/config.js`. +{%change%} Add the following line below the `const config = {` line in your `src/config.ts`. -```txt -STRIPE_KEY: "YOUR_STRIPE_PUBLIC_KEY", +```typescript +STRIPE_KEY: "", ``` Make sure to replace, `YOUR_STRIPE_PUBLIC_KEY` with the **Publishable key** from the [Setup a Stripe account]({% link _chapters/setup-a-stripe-account.md %}) chapter. Let's also add the Stripe.js packages -{%change%} Run the following in the `frontend/` directory and **not** in your project root. +{%change%} Run the following **in the `frontend/` directory** and **not** in your project root. ```bash -$ npm install @stripe/stripe-js +$ pnpm add @stripe/stripe-js --save ``` And load the Stripe config in our settings page. -{%change%} Add the following at top of the `Settings` component in `src/containers/Settings.js` above the `billUser()` function. +{%change%} Add the following at top of the `Settings` component in `src/containers/Settings.tsx` above the `billUser()` function. -```js +```typescript const stripePromise = loadStripe(config.STRIPE_KEY); ``` This loads the Stripe object from Stripe.js with the Stripe key when our settings page loads. We'll be using this in the coming chapters. -{%change%} We'll also import this function at the top. +{%change%} We'll also import `stripe` & `config` at the top. -```js +```typescript import { loadStripe } from "@stripe/stripe-js"; ``` diff --git a/_chapters/add-the-create-note-page.md b/_chapters/add-the-create-note-page.md index 4ea51573c..dc8d901bf 100644 --- a/_chapters/add-the-create-note-page.md +++ b/_chapters/add-the-create-note-page.md @@ -14,19 +14,18 @@ First we are going to create the form for a note. It'll take some content and a ### Add the Container -{%change%} Create a new file `src/containers/NewNote.js` and add the following. +{%change%} Create a new file `src/containers/NewNote.tsx` and add the following. -```jsx -import React, { useRef, useState } from "react"; +```tsx +import React, {useRef, useState} from "react"; import Form from "react-bootstrap/Form"; -import { useNavigate } from "react-router-dom"; +import {useNavigate} from "react-router-dom"; import LoaderButton from "../components/LoaderButton"; -import { onError } from "../lib/errorLib"; import config from "../config"; import "./NewNote.css"; export default function NewNote() { - const file = useRef(null); + const file = useRef(null); const nav = useNavigate(); const [content, setContent] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -35,11 +34,12 @@ export default function NewNote() { return content.length > 0; } - function handleFileChange(event) { - file.current = event.target.files[0]; + function handleFileChange(event: React.ChangeEvent) { + if ( event.currentTarget.files === null ) return + file.current = event.currentTarget.files[0]; } - async function handleSubmit(event) { + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { @@ -82,19 +82,21 @@ export default function NewNote() { ); } + ``` Everything is fairly standard here, except for the file input. Our form elements so far have been [controlled components](https://facebook.github.io/react/docs/forms.html), as in their value is directly controlled by the state of the component. However, in the case of the file input we want the browser to handle this state. So instead of `useState` we'll use the `useRef` hook. The main difference between the two is that `useRef` does not cause the component to re-render. It simply tells React to store a value for us so that we can use it later. We can set/get the current value of a ref by using its `current` property. Just as we do when the user selects a file. -```js +```typescript file.current = event.target.files[0]; ``` Currently, our `handleSubmit` does not do a whole lot other than limiting the file size of our attachment. We are going to define this in our config. -{%change%} So add the following to our `src/config.js` below the `const config = {` line. +{%change%} So add the following to our `src/config.ts` below the `const config = {` line. -```txt +```typescript +// Frontend config MAX_ATTACHMENT_SIZE: 5000000, ``` @@ -109,15 +111,15 @@ MAX_ATTACHMENT_SIZE: 5000000, ### Add the Route -{%change%} Finally, add our container as a route in `src/Routes.js` below our signup route. +{%change%} Finally, add our container as a route in `src/Routes.tsx` below our signup route. -```jsx +```tsx } /> ``` {%change%} And include our component in the header. -```js +```tsx import NewNote from "./containers/NewNote"; ``` diff --git a/_chapters/add-the-session-to-the-state.md b/_chapters/add-the-session-to-the-state.md index 68d3bff91..8a2408fb3 100644 --- a/_chapters/add-the-session-to-the-state.md +++ b/_chapters/add-the-session-to-the-state.md @@ -3,7 +3,7 @@ layout: post title: Add the Session to the State date: 2017-01-15 00:00:00 lang: en -comments_id: add-the-session-to-the-state +ref: add-the-session-to-the-state redirect_from: /chapters/add-the-user-token-to-the-state.html description: We need to add the user session to the state of our App component in our React.js app. We are going to use React context through the useContext hook to store it and pass it to all our child components. comments_id: add-the-session-to-the-state/136 @@ -15,31 +15,31 @@ To complete the login process we would need to update the app state with the ses First we'll start by updating the application state by setting that the user is logged in. We might be tempted to store this in the `Login` container, but since we are going to use this in a lot of other places, it makes sense to lift up the state. The most logical place to do this will be in our `App` component. -To save the user's login state, let's include the `useState` hook in `src/App.js`. +To save the user's login state, let's include the `useState` hook in `src/App.tsx`. -{%change%} Replace the `React` import: +{%change%} Add the following to the top of our `App` component function. -```js -import React from "react"; +```tsx +const [isAuthenticated, userHasAuthenticated] = useState(false); ``` -{%change%} With the following: +Then, replace the `React` import: -```js -import React, { useState } from "react"; +```tsx +import React from "react"; ``` -{%change%} Add the following to the top of our `App` component function. +{%change%} with the following: -```js -const [isAuthenticated, userHasAuthenticated] = useState(false); +```tsx +import React, { useState } from "react"; ``` This initializes the `isAuthenticated` state variable to `false`, as in the user is not logged in. And calling `userHasAuthenticated` updates it. But for the `Login` container to call this method we need to pass a reference of this method to it. ### Store the Session in the Context -We are going to have to pass the session related info to all of our containers. This is going to be tedious if we pass it in as a prop, since we'll have to do that manually for each component. Instead let's use [React Context](https://reactjs.org/docs/context.html) for this. +We are going to have to pass the session related info to all of our containers. This is going to be tedious if we pass it in as a prop, since we'll have to do that manually for each component. Instead let's use [React Context](https://reactjs.org/docs/context.html){:target="_blank"} for this. We'll create a context for our entire app that all of our containers will use. @@ -51,16 +51,25 @@ $ mkdir src/lib/ We'll use this to store all our common code. -{%change%} Add the following to `src/lib/contextLib.js`. +{%change%} Add the following file with the content below `src/lib/contextLib.ts`. -```js -import { useContext, createContext } from "react"; +```typescript +import React, {createContext, useContext} from "react"; + +export interface AppContextType { + isAuthenticated: boolean; + userHasAuthenticated: React.Dispatch>; +} -export const AppContext = createContext(null); +export const AppContext = createContext({ + isAuthenticated: false, + userHasAuthenticated: useAppContext +}); export function useAppContext() { - return useContext(AppContext); + return useContext(AppContext); } + ``` This really simple bit of code is creating and exporting two things: @@ -70,17 +79,17 @@ This really simple bit of code is creating and exporting two things: If you are not sure how Contexts work, don't worry, it'll make more sense once we use it. -{%change%} Import our new app context in the header of `src/App.js`. +{%change%} Import our new app context in the header of `src/App.tsx`. -```js -import { AppContext } from "./lib/contextLib"; +```tsx +import { AppContext, AppContextType } from "./lib/contextLib"; ``` Now to add our session to the context and to pass it to our containers: -{%change%} Wrap our `Routes` component in the `return` statement of `src/App.js`. +{%change%} Wrap our `Routes` component in the `return` statement of `src/App.tsx`. -```jsx +```tsx ``` @@ -88,9 +97,9 @@ Now to add our session to the context and to pass it to our containers: {% raw %} -```jsx - - +```tsx + + ``` @@ -98,7 +107,7 @@ Now to add our session to the context and to pass it to our containers: React Context's are made up of two parts. The first is the Provider. This is telling React that all the child components inside the Context Provider should be able to access what we put in it. In this case we are putting in the following object: -```js +```tsx { isAuthenticated, userHasAuthenticated; } @@ -106,33 +115,33 @@ React Context's are made up of two parts. The first is the Provider. This is tel ### Use the Context to Update the State -The second part of the Context API is the consumer. We'll add that to the Login container: +The second part of the Context API is the consumer. We'll add that to the Login container (src/containers/Login.tsx): -{%change%} Start by importing it in the header of `src/containers/Login.js`. +{%change%} Include the hook by adding it below the `export default function Login() {` line. -```js -import { useAppContext } from "../lib/contextLib"; +```tsx +const { userHasAuthenticated } = useAppContext(); ``` -{%change%} Include the hook by adding it below the `export default function Login() {` line. +{%change%} And import it in the header of `src/containers/Login.tsx`. -```js -const { userHasAuthenticated } = useAppContext(); +```tsx +import { useAppContext } from "../lib/contextLib"; ``` This is telling React that we want to use our app context here and that we want to be able to use the `userHasAuthenticated` function. -{%change%} Finally, replace the `alert('Logged in');` line with the following in `src/containers/Login.js`. +{%change%} Finally, replace the `alert('Logged in');` line with the following in `src/containers/Login.tsx`. -```js +```tsx userHasAuthenticated(true); ``` ### Create a Logout Button -We can now use this to display a Logout button once the user logs in. Find the following in our `src/App.js`. +We can now use this to display a Logout button once the user logs in. Find the following in our `src/App.tsx`. -```jsx +```tsx Signup @@ -143,7 +152,7 @@ We can now use this to display a Logout button once the user logs in. Find the f {%change%} And replace it with this: -```jsx +```tsx {isAuthenticated ? ( Logout ) : ( @@ -158,17 +167,17 @@ We can now use this to display a Logout button once the user logs in. Find the f )} ``` -The `<>` or [Fragment component](https://reactjs.org/docs/fragments.html) can be thought of as a placeholder component. We need this because in the case the user is not logged in, we want to render two links. To do this we would need to wrap it inside a single component, like a `div`. But by using the Fragment component it tells React that the two links are inside this component but we don't want to render any extra HTML. +The `<>` or [Fragment component](https://reactjs.org/docs/fragments.html){:target="_blank"} can be thought of as a placeholder component. We need this because in the case the user is not logged in, we want to render two links. To do this we would need to wrap it inside a single component, like a `div`. But by using the Fragment component it tells React that the two links are inside this component but we don't want to render any extra HTML. -{%change%} And add this `handleLogout` method to `src/App.js` above the `return` statement as well. +{%change%} And add this `handleLogout` method to `src/App.tsx` above the `return` statement as well. -```js +```tsx function handleLogout() { userHasAuthenticated(false); } ``` -Now head over to your browser and try logging in with the admin credentials we created in the [Secure Our Serverless APIs]({% link _chapters/secure-our-serverless-apis.md %}) chapter. You should see the Logout button appear right away. +Now head over to your browser and try logging in with the admin credentials we created in the [Secure Our Serverless APIs]({% link _chapters/secure-our-serverless-apis.md %}){:target="_blank"} chapter. You should see the Logout button appear right away. ![Login state updated screenshot](/assets/login-state-updated.png) diff --git a/_chapters/adding-auth-to-our-serverless-app.md b/_chapters/adding-auth-to-our-serverless-app.md index 8f0dd9178..4c2f433f6 100644 --- a/_chapters/adding-auth-to-our-serverless-app.md +++ b/_chapters/adding-auth-to-our-serverless-app.md @@ -11,60 +11,65 @@ ref: adding-auth-to-our-serverless-app comments_id: adding-auth-to-our-serverless-app/2457 --- -So far we've created the [DynamoDB table]({% link _chapters/create-a-dynamodb-table-in-sst.md %}), [S3 bucket]({% link _chapters/create-an-s3-bucket-in-sst.md %}), and [API]({% link _chapters/add-an-api-to-create-a-note.md %}) parts of our serverless backend. Now let's add auth into the mix. As we talked about in the [previous chapter]({% link _chapters/auth-in-serverless-apps.md %}), we are going to use [Cognito User Pool](https://aws.amazon.com/cognito/) to manage user sign ups and logins. While we are going to use [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html) to manage which resources our users have access to. +So far we've created the [DynamoDB table]({% link _chapters/create-a-dynamodb-table-in-sst.md %}), [S3 bucket]({% link _chapters/create-an-s3-bucket-in-sst.md %}), and [API]({% link _chapters/add-an-api-to-create-a-note.md %}) parts of our serverless backend. Now let's add auth into the mix. As we talked about in the [previous chapter]({% link _chapters/auth-in-serverless-apps.md %}), we are going to use [Cognito User Pool](https://aws.amazon.com/cognito/){:target="_blank"} to manage user sign ups and logins. While we are going to use [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html){:target="_blank"} to manage which resources our users have access to. -Setting this all up can be pretty complicated in CDK. SST has a simple [`Auth`]({{ site.docs_url }}/constructs/Auth) construct to help with this. +Setting this all up can be pretty complicated in CDK. SST has a simple [`Auth`]({{ site.docs_url }}/constructs/Auth){:target="_blank"} construct to help with this. ### Create a Stack -{%change%} Add the following to a new file in `stacks/AuthStack.js`. +{%change%} Add the following to a new file in `stacks/AuthStack.ts`. -```js +```typescript import * as iam from "aws-cdk-lib/aws-iam"; -import { Cognito, use } from "sst/constructs"; -import { StorageStack } from "./StorageStack"; -import { ApiStack } from "./ApiStack"; - -export function AuthStack({ stack, app }) { - const { bucket } = use(StorageStack); - const { api } = use(ApiStack); - - // Create a Cognito User Pool and Identity Pool - const auth = new Cognito(stack, "Auth", { - login: ["email"], - }); - - auth.attachPermissionsForAuthUsers(stack, [ - // Allow access to the API - api, - // Policy granting access to a specific folder in the bucket - new iam.PolicyStatement({ - actions: ["s3:*"], - effect: iam.Effect.ALLOW, - resources: [ - bucket.bucketArn + "/private/${cognito-identity.amazonaws.com:sub}/*", - ], - }), - ]); - - // Show the auth resources in the output - stack.addOutputs({ - Region: app.region, - UserPoolId: auth.userPoolId, - IdentityPoolId: auth.cognitoIdentityPoolId, - UserPoolClientId: auth.userPoolClientId, - }); - - // Return the auth resource - return { - auth, - }; +import {Cognito, StackContext, use} from "sst/constructs"; +import {StorageStack} from "./StorageStack"; +import {ApiStack} from "./ApiStack"; + +export function AuthStack({ stack, app }: StackContext) { + const { bucket } = use(StorageStack); + const { api } = use(ApiStack); + + // Create a Cognito User Pool and Identity Pool + const auth = new Cognito(stack, "Auth", { + login: ["email"], + }); + + auth.attachPermissionsForAuthUsers(stack, [ + // Allow access to the API + api, + // Policy granting access to a specific folder in the bucket + new iam.PolicyStatement({ + actions: ["s3:*"], + effect: iam.Effect.ALLOW, + resources: [ + bucket.bucketArn + "/private/${cognito-identity.amazonaws.com:sub}/*", + ], + }), + ]); + + // Show the auth resources in the output + stack.addOutputs({ + Region: app.region, + UserPoolId: auth.userPoolId, + IdentityPoolId: auth.cognitoIdentityPoolId, + UserPoolClientId: auth.userPoolClientId, + }); + + // Return the auth resource + return { + auth, + }; } + ``` -Let's quickly go over what we are doing here. +Let's go over what we are doing here. + +{% aside %} +Learn more about sharing resources between stacks [here]({{ site.docs_url }}/constructs/Stack#sharing-resources-between-stacks){:target="_blank"}. +{% endaside %} -- We are creating a new stack for our auth infrastructure. We don't need to create a separate stack but we are using it as an example to show how to work with multiple stacks. +- We are creating a new stack for our auth infrastructure. While we don't need to create a separate stack, we are using it as an example to show how to work with multiple stacks. Additionally, such separations can make maintenance easier. - The `Auth` construct creates a Cognito User Pool for us. We are using the `login` prop to state that we want our users to login with their email. @@ -74,15 +79,13 @@ Let's quickly go over what we are doing here. - And we want them to access our S3 bucket. We'll look at this in detail below. -- Finally, we output the ids of the auth resources that've been created and returning the auth resource so that other stacks can access this resource. - -Note, learn more about sharing resources between stacks [here](https://docs.sst.dev/constructs/Stack#sharing-resources-between-stacks). +- Finally, we output the ids of the auth resources that have been created and returning the auth resource so that other stacks can access this resource. ### Securing Access to Uploaded Files We are creating a specific IAM policy to secure the files our users will upload to our S3 bucket. -```js +```typescript // Policy granting access to a specific folder in the bucket new iam.PolicyStatement({ actions: ["s3:*"], @@ -101,19 +104,18 @@ One other thing to note is that, the federated identity id is a UUID that is ass ### Add to the App -Let's add this stack to our app. +Let's add this stack to our app config in file `sst.config.ts` . -{%change%} Replace the `stacks` function in `sst.config.ts` with this. +{%change%} Replace the `stacks` function with this line that adds the AuthStack into our chain of stacks. -```js +```typescript stacks(app) { app.stack(StorageStack).stack(ApiStack).stack(AuthStack); }, ``` +{%change%} And import the new stack at the top of the file. -{%change%} Also, import the new stack at the top. - -```js +```typescript import { AuthStack } from "./stacks/AuthStack"; ``` @@ -121,21 +123,15 @@ import { AuthStack } from "./stacks/AuthStack"; We also need to enable authentication in our API. -{%change%} Add the following above the `function: {` line in `stacks/ApiStack.js`. +{%change%} Add the following key into the defaults hash above the `function: {` line in `stacks/ApiStack.ts`. -```js +```typescript authorizer: "iam", ``` This tells our API that we want to use `AWS_IAM` across all our routes. -### Deploy the App - -If you switch over to your terminal, you'll notice that your changes are being deployed. - -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. - -You should see something like this at the end of the deploy process. +{%deploy%} ```bash ✓ Deployed: @@ -149,13 +145,13 @@ You should see something like this at the end of the deploy process. UserPoolId: us-east-1_TYEz7XP7P ``` -You'll also see our new User Pool if you head over to the **Cognito** tab in the [SST Console]({{ site.old_console_url }}). +You'll also see our new User Pool if you head over to the **Cognito** tab in the [SST Console]({{ site.old_console_url }}){:target="_blank"}. ![SST Console Cognito tab](/assets/part2/sst-console-cognito-tab.png) ### Create a Test User -Let's create a test user so that we can test our API. Click the **Create User** button. +Let's create a test user so that we can test our API. Click the **Create User** button in the SST Console. {%change%} Fill in `admin@example.com` as the **Email** and `Passw0rd!` as the **Password**, then hit **Create**. diff --git a/_chapters/adding-links-in-the-navbar.md b/_chapters/adding-links-in-the-navbar.md index 342690b6f..c59b6c0fb 100644 --- a/_chapters/adding-links-in-the-navbar.md +++ b/_chapters/adding-links-in-the-navbar.md @@ -10,9 +10,9 @@ comments_id: adding-links-in-the-navbar/141 Now that we have our first route set up, let's add a couple of links to the navbar of our app. These will direct users to login or signup for our app when they first visit it. -{%change%} Replace the `App` function component in `src/App.js` with the following. +{%change%} Replace the `App` function component in `src/App.tsx` with the following. -```jsx +```tsx function App() { return (

@@ -40,9 +40,9 @@ We also added a link to the _Scratch_ logo. It links back to the homepage of our And let's include the `Nav` component in the header. -{%change%} Add the following import to the top of your `src/App.js`. +{%change%} Add the following import to the top of your `src/App.tsx`. -```jsx +```tsx import Nav from "react-bootstrap/Nav"; ``` @@ -52,25 +52,20 @@ Now if you flip over to your browser, you should see the links in our navbar. Unfortunately, when you click on them they refresh your browser while redirecting to the link. We need it to route it to the new link without refreshing the page since we are building a single page app. -To fix this we need a component that works with React Router and React Bootstrap called [React Router Bootstrap](https://github.com/react-bootstrap/react-router-bootstrap). It can wrap around your `Navbar` links and use the React Router to route your app to the required link without refreshing the browser. +To fix this we need a component that works with React Router and React Bootstrap called [React Router Bootstrap](https://github.com/react-bootstrap/react-router-bootstrap){:target="_blank"}. It can wrap around your `Navbar` links and use the React Router to route your app to the required link without refreshing the browser. {%change%} Run the following command in the `frontend/` directory and **not** in your project root. ```bash -$ npm install react-router-bootstrap +$ pnpm add --save react-router-bootstrap;pnpm add --save-dev @types/react-router-bootstrap ``` +{%aside%} +You may need to restart the frontend script after this step. +{%endaside%} -Let's also import it. - -{%change%} Add this to the top of your `src/App.js`. - -```jsx -import { LinkContainer } from "react-router-bootstrap"; -``` - -{%change%} We will now wrap our links with the `LinkContainer`. Replace the `App` function component in your `src/App.js` with this. +{%change%} We will now wrap our links with the `LinkContainer`. Replace the `App` function component in your `src/App.tsx` with this. -```jsx +```tsx function App() { return (
@@ -96,9 +91,17 @@ function App() { } ``` +Let's also import it. + +{%change%} Add this to the top of your `src/App.tsx`. + +```tsx +import { LinkContainer } from "react-router-bootstrap"; +``` + We are doing one other thing here. We are grabbing the current path the user is on from the `window.location` object. And we set it as the `activeKey` of our `Nav` component. This'll highlight the link when we are on that page. -```jsx +```tsx - +
- ) ); ``` @@ -99,7 +108,7 @@ return ( You'll notice that we added another link in the navbar that only displays when a user is logged in. -```jsx +```tsx Settings diff --git a/_chapters/create-an-aws-account.md b/_chapters/create-an-aws-account.md index 7495d6cce..5ec172589 100644 --- a/_chapters/create-an-aws-account.md +++ b/_chapters/create-an-aws-account.md @@ -8,8 +8,6 @@ description: To create a serverless app using Lambda we are going to first need comments_id: create-an-aws-account/88 --- -Let's first get started by creating an AWS (Amazon Web Services) account. Of course you can skip this if you already have one. Head over to the [AWS homepage](https://aws.amazon.com) and hit the **Create a Free Account** and follow the steps to create your account. +Let's first get started by creating an AWS (Amazon Web Services) account. Of course you can skip this if you already have one. Head over to the [AWS homepage](https://aws.amazon.com){:target="_blank"} and create your account. -![Create an aws account Screenshot](/assets/create-an-aws-account.png) - -Next let's configure your account so it's ready to be used for the rest of our guide. +Next we'll configure your account so it's ready to be used for the rest of our guide. diff --git a/_chapters/create-an-iam-user.md b/_chapters/create-an-iam-user.md index f308e8c1e..ac602b693 100644 --- a/_chapters/create-an-iam-user.md +++ b/_chapters/create-an-iam-user.md @@ -16,48 +16,68 @@ In this chapter, we are going to create a new IAM user for a couple of the AWS r ### Create User -First, log in to your [AWS Console](https://console.aws.amazon.com) and select IAM from the list of services. +First, log in to your [AWS Console](https://console.aws.amazon.com) and search for IAM in the search bar. Hover or focus on the **IAM card** and then select the **Users** link. -![Select IAM Service Screenshot](/assets/iam-user/select-iam-service.png) +![Select IAM Service Screenshot](/assets/create-iam-user/search-to-iam-service.png) -Select **Users**. +Select **Add Users**. -![Select IAM Users Screenshot](/assets/iam-user/select-iam-users.png) +![Add IAM User Screenshot](/assets/create-iam-user/add-iam-user-button.png) -Select **Add User**. +Enter a **User name**, then select **Next**. -![Add IAM User Screenshot](/assets/iam-user/add-iam-user.png) +This account will be used by our [AWS CLI](https://aws.amazon.com/cli/) and [SST]({{ site.sst_github_repo }}). They will be connecting to the AWS API directly and will not be using the Management Console. -Enter a **User name** and check **Programmatic access**, then select **Next: Permissions**. +{%note%} +It is best practice to avoid creating keys when possible. When using programmatic access keys, regularly rotate them. In most cases, there are alternative solutions, see the [AWS IAM User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey) for more information. +{%endnote%} -This account will be used by our [AWS CLI](https://aws.amazon.com/cli/) and [SST]({{ site.sst_github_repo }}). They'll be connecting to the AWS API directly and will not be using the Management Console. - -![Fill in IAM User Info Screenshot](/assets/iam-user/fill-in-iam-user-info.png) +![Fill in IAM User Info Screenshot](/assets/create-iam-user/fill-in-iam-user-details.png) Select **Attach existing policies directly**. -![Add IAM User Policy Screenshot](/assets/iam-user/add-iam-user-policy.png) +![Add IAM User Policy Screenshot](/assets/create-iam-user/add-iam-attach-policies-directly.png) -Search for **AdministratorAccess** and select the policy, then select **Next: Tags**. +Search for **AdministratorAccess** and select the policy by checking the checkbox, then select **Next**. -We can provide a more fine-grained policy here and we cover this later in the [Customize the Serverless IAM Policy]({% link _chapters/customize-the-serverless-iam-policy.md %}) chapter. But for now, let's continue with this. +We can provide a more fine-grained policy here. We cover this later in the [Customize the Serverless IAM Policy]({% link _chapters/customize-the-serverless-iam-policy.md %}) chapter. But for now, let's continue with this. -![Added Admin Policy Screenshot](/assets/iam-user/added-admin-policy.png) +![Added Admin Policy Screenshot](/assets/create-iam-user/iam-user-add-admin-policy.png) -We can optionally add some info to our IAM user. But we'll skip this for now. Click **Next: Review**. +Select **Create user**. -![Skip IAM tags Screenshot](/assets/iam-user/skip-iam-tags.png) +![Reivew IAM User Screenshot](/assets/create-iam-user/iam-create-user.png) -Select **Create user**. +Select **View user**. + +![View IAM User Screenshot](/assets/create-iam-user/iam-success-view-user.png) + +Select **Security credentials** + +![IAM User Security Credentials Screenshot](/assets/create-iam-user/iam-user-security-credentials.png) -![Reivew IAM User Screenshot](/assets/iam-user/review-iam-user.png) +Select **Create access key** + +![IAM User Create Access Key Screenshot](/assets/create-iam-user/iam-user-create-access-key.png) + +In keeping with the current guide instructions, we will choose other to generate an access key and secret. Select **Other** and select **Next** + +![IAM User Access Key Purpose](/assets/create-iam-user/iam-user-access-key-purpose.png) + +You could add a descriptive tag here, but we will skip that in this tutorial, select **Create access key** + +![IAM User Access Key Purpose](/assets/create-iam-user/iam-access-key-skip-tag-create.png) Select **Show** to reveal **Secret access key**. -![Added IAM User Screenshot](/assets/iam-user/added-iam-user.png) +![IAM User Access Key Show](/assets/create-iam-user/iam-access-key-secret-show.png) + +{%note%} +This is the only screen on which you will be able to access this key. Save it to a secure location using best practices to ensure the security of your application. +{%endnote%} -Take a note of the **Access key ID** and **Secret access key**. We will be needing this in the next chapter. +Take a note of the **Access key** and **Secret access key**. We will be needing this in the next chapter. -![IAM User Credentials Screenshot](/assets/iam-user/iam-user-credentials.png) +![IAM Access Credentials Screenshot](/assets/create-iam-user/iam-access-credentials.png) -Now let's configure our AWS CLI so we can deploy our applications from our command line. +Now let's configure our AWS CLI. By configuring the AWS CLI, we can deploy our applications from our command line. diff --git a/_chapters/create-an-s3-bucket-in-sst.md b/_chapters/create-an-s3-bucket-in-sst.md index 97800371f..2ddedfe75 100644 --- a/_chapters/create-an-s3-bucket-in-sst.md +++ b/_chapters/create-an-s3-bucket-in-sst.md @@ -3,7 +3,7 @@ layout: post title: Create an S3 Bucket in SST date: 2021-07-17 00:00:00 lang: en -description: In this chapter we'll be using a higher-level CDK construct to create an S3 bucket in our SST app. +description: In this chapter we will be using a higher-level CDK construct to create an S3 bucket in our SST app. redirect_from: /chapters/configure-s3-in-cdk.html ref: create-an-s3-bucket-in-sst comments_id: create-an-s3-bucket-in-sst/2461 @@ -11,48 +11,51 @@ comments_id: create-an-s3-bucket-in-sst/2461 Just like [the previous chapter]({% link _chapters/create-a-dynamodb-table-in-sst.md %}), we are going to be using [AWS CDK]({% link _chapters/what-is-aws-cdk.md %}) in our SST app to create an S3 bucket. -We'll be adding to the `StorageStack` that we created. +We will be adding to the `StorageStack` that we created. ### Add to the Stack -{%change%} Add the following above the `Table` definition in `stacks/StorageStack.js`. +{%change%} Add the following inside the `StorageStack` function above the `Table` definition in `stacks/StorageStack.ts`. -```js +```typescript // Create an S3 bucket const bucket = new Bucket(stack, "Uploads"); ``` -{%change%} Make sure to import the `Bucket` construct. Replace the import line up top with this. +{%change%} Make sure to import the `Bucket` construct by replacing the import line at the top with the line below. -```js -import { Bucket, Table } from "sst/constructs"; +```typescript +import { Bucket, StackContext, Table } from "sst/constructs"; ``` This creates a new S3 bucket using the SST [`Bucket`]({{ site.docs_url }}/constructs/Bucket) construct. -Also, find the following line in `stacks/StorageStack.js`. +Also, find the following line in `stacks/StorageStack.ts`. -```js +```typescript return { table, }; ``` -{%change%} And add the `bucket` below `table`. +{%change%} And add the `bucket` above `table` matching the code below. -```js - bucket, +```typescript +return { + bucket, + table, +}; ``` -This'll allow us to reference the S3 bucket in other stacks. +This will allow us to reference the S3 bucket in other stacks. -Note, learn more about sharing resources between stacks [here](https://docs.sst.dev/constructs/Stack#sharing-resources-between-stacks). +Note, learn more about sharing resources between stacks [here](https://docs.sst.dev/constructs/Stack#sharing-resources-between-stacks){:target="_blank"}. ### Deploy the App -If you switch over to your terminal, you'll notice that your changes are being deployed. +If you switch over to your terminal, you will notice that your changes are being deployed. -Note that, you'll need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. +Note that, you will need to have `sst dev` running for this to happen. If you had previously stopped it, then running `npx sst dev` will deploy your changes again. You should see that the storage stack has been updated. @@ -61,7 +64,7 @@ You should see that the storage stack has been updated. StorageStack ``` -You can also head over to the **Buckets** tab in the [SST Console]({{ site.old_console_url }}) and check out the new bucket. +You can also head over to the **Buckets** tab in the [SST Console]({{ site.old_console_url }}){:target="_blank"} and check out the new bucket. ![SST Console Buckets tab](/assets/part2/sst-console-buckets-tab.png) diff --git a/_chapters/create-an-s3-bucket.md b/_chapters/create-an-s3-bucket.md index e508412e8..bb2827b33 100644 --- a/_chapters/create-an-s3-bucket.md +++ b/_chapters/create-an-s3-bucket.md @@ -47,7 +47,7 @@ Buckets by default are not publicly accessible, so we need to change the S3 Buck {%change%} Add the following bucket policy into the editor. Where `notes-app-client` is the name of our S3 bucket. Make sure to use the name of your bucket here. -``` json +```json { "Version":"2012-10-17", "Statement":[{ diff --git a/_chapters/create-an-sst-app.md b/_chapters/create-an-sst-app.md index adae0f566..8b4ed9633 100644 --- a/_chapters/create-an-sst-app.md +++ b/_chapters/create-an-sst-app.md @@ -14,14 +14,14 @@ Now that we understand what _infrastructure as code_ is, we are ready to create {%change%} Run the following in your working directory. ```bash -$ npx create-sst@latest notes +$ pnpm dlx create-sst@latest notes $ cd notes -$ npm install +$ pnpm install ``` By default, our app will be deployed to the `us-east-1` AWS region. This can be changed in the `sst.config.ts` in your project root. -```ts +```typescript import { SSTConfig } from "sst"; import { API } from "./stacks/MyStack"; diff --git a/_chapters/create-containers.md b/_chapters/create-containers.md index 57107fafc..4082dadf8 100644 --- a/_chapters/create-containers.md +++ b/_chapters/create-containers.md @@ -14,9 +14,9 @@ Currently, our app has a single component that renders our content. For creating Let's start by creating the outer chrome of our application by first adding a navigation bar to it. We are going to use the [Navbar](https://react-bootstrap.github.io/components/navbar/) React-Bootstrap component. -{%change%} Go ahead and remove the code inside `src/App.js` and replace it with the following. +{%change%} Go ahead and remove the code inside `src/App.tsx` and replace it with the following. -```jsx +```tsx import React from "react"; import Navbar from "react-bootstrap/Navbar"; import "./App.css"; @@ -53,17 +53,17 @@ For now we don't have any styles to add but we'll leave this file around, in cas Also, let's remove some unused template files. -{%change%} Run the following in your React `frontend/` directory. +{%change%} Run the following **in the `frontend/` directory**. ```bash -$ rm src/logo.svg src/App.test.js +$ rm src/logo.svg src/App.test.tsx ``` ### Add the Home container Now that we have the outer chrome of our application ready, let's add the container for the homepage of our app. It'll respond to the `/` route. -{%change%} Create a `src/containers/` directory by running the following in the `frontend/` directory. +{%change%} Create a `src/containers/` directory by running the following **in the `frontend/` directory**. ```bash $ mkdir src/containers/ @@ -71,9 +71,9 @@ $ mkdir src/containers/ We'll be storing all of our top level components here. These are components that will respond to our routes and make requests to our API. We will be calling them _containers_ through the rest of this tutorial. -{%change%} Create a new container and add the following to `src/containers/Home.js`. +{%change%} Create a new container and add the following to `src/containers/Home.tsx`. -```jsx +```tsx import React from "react"; import "./Home.css"; @@ -89,7 +89,7 @@ export default function Home() { } ``` -This simply renders our homepage given that the user is not currently signed in. +This renders our homepage given that the user is not currently signed in. Now let's add a few lines to style this. @@ -111,9 +111,9 @@ Now let's add a few lines to style this. Now we'll set up the routes so that we can have this container respond to the `/` route. -{%change%} Create `src/Routes.js` and add the following into it. +{%change%} Create `src/Routes.tsx` and add the following into it. -```jsx +```tsx import React from "react"; import { Route, Routes } from "react-router-dom"; import Home from "./containers/Home"; @@ -133,21 +133,21 @@ This component uses this `Routes` component from React-Router that renders the f Now let's render the routes into our App component. -{%change%} Add the following to the header of your `src/App.js`. +{%change%} Add the following to the header of your `src/App.tsx`. -```jsx +```tsx import Routes from "./Routes"; ``` -{%change%} And add the following line below our `Navbar` component inside `src/App.js`. +{%change%} And add the following line below our `Navbar` component inside `src/App.tsx`. -```jsx +```tsx ``` -So the `App` function component of our `src/App.js` should now look like this. +So the `App` function component of our `src/App.tsx` should now look like this. -```jsx +```tsx function App() { return (
diff --git a/_chapters/create-the-signup-form.md b/_chapters/create-the-signup-form.md index 2eb5b37de..d12757c00 100644 --- a/_chapters/create-the-signup-form.md +++ b/_chapters/create-the-signup-form.md @@ -4,7 +4,7 @@ title: Create the Signup Form date: 2017-01-20 00:00:00 lang: en ref: create-the-signup-form -description: We are going to create a signup page for our React.js app. To sign up users with Amazon Cognito, we need to create a form that allows users to enter a cofirmation code that is emailed to them. +description: We are going to create a signup page for our React.js app. To sign up users with Amazon Cognito, we need to create a form that allows users to enter a confirmation code that is emailed to them. comments_id: create-the-signup-form/52 --- @@ -12,16 +12,16 @@ Let's start by creating the signup form that'll get the user's email and passwor ### Add the Container -{%change%} Create a new container at `src/containers/Signup.js` with the following. +{%change%} Create a new container at `src/containers/Signup.tsx` with the following. -```jsx -import React, { useState } from "react"; +```tsx +import React, {useState} from "react"; import Form from "react-bootstrap/Form"; -import { useNavigate } from "react-router-dom"; +import {useNavigate} from "react-router-dom"; import LoaderButton from "../components/LoaderButton"; -import { useAppContext } from "../lib/contextLib"; -import { useFormFields } from "../lib/hooksLib"; -import { onError } from "../lib/errorLib"; +import {useAppContext} from "../lib/contextLib"; +import {ISignUpResult} from "amazon-cognito-identity-js"; +import {useFormFields} from "../lib/hooksLib"; import "./Signup.css"; export default function Signup() { @@ -32,7 +32,7 @@ export default function Signup() { confirmationCode: "", }); const nav = useNavigate(); - const [newUser, setNewUser] = useState(null); + const [newUser, setNewUser] = useState(null); const { userHasAuthenticated } = useAppContext(); const [isLoading, setIsLoading] = useState(false); @@ -48,14 +48,14 @@ export default function Signup() { return fields.confirmationCode.length > 0; } - async function handleSubmit(event) { + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setIsLoading(true); - setNewUser("test"); + setNewUser(fields); setIsLoading(false); } - async function handleConfirmationSubmit(event) { + async function handleConfirmationSubmit(event: React.FormEvent) { event.preventDefault(); setIsLoading(true); } @@ -63,7 +63,7 @@ export default function Signup() { function renderConfirmationForm() { return (
- + Confirmation Code - + Email - + Password - + Confirm Password ); } + ``` Most of the things we are doing here are fairly straightforward but let's go over them quickly. @@ -153,7 +154,7 @@ Most of the things we are doing here are fairly straightforward but let's go ove 4. We are setting the `autoFocus` flags on the email and the confirmation code fields. - ```coffee + ```tsx } /> ``` {%change%} And include our component in the header. -```js +```typescript import Signup from "./containers/Signup"; ``` -Now if we switch to our browser and navigate to the signup page we should see our newly created form. Our form doesn't do anything when we enter in our info but you can still try to fill in an email address, password, and the confirmation code. It'll give you an idea of how the form will behave once we connect it to Cognito. +Now if we switch to our browser and navigate to the signup page we should see our newly created form. Our form doesn't do anything when we enter in our info but you can still try to fill in an email address, password, and confirmation password. ![Signup page added screenshot](/assets/signup-page-added.png) +Then, after hitting submit, you'll get the confirmation code form. This'll give you an idea of how the form will behave once we connect it to Cognito. + +![Signup page added screenshot](/assets/signup-page-confirmation-code.png) + Next, let's connect our signup form to Amazon Cognito. diff --git a/_chapters/creating-a-ci-cd-pipeline-for-serverless.md b/_chapters/creating-a-ci-cd-pipeline-for-serverless.md index a11048fec..bd117ed98 100644 --- a/_chapters/creating-a-ci-cd-pipeline-for-serverless.md +++ b/_chapters/creating-a-ci-cd-pipeline-for-serverless.md @@ -20,7 +20,7 @@ So to recap, here's what we've created so far. - [A way to run unit tests for our infrastructure and functions]({% link _chapters/unit-tests-in-serverless.md %}) - [Deployed to a prod environment with a custom domain]({% link _chapters/custom-domains-in-serverless-apis.md %}) -All of this is neatly [committed in a Git repo]({{ site.sst_demo_repo }}). +All of this is neatly [committed in a Git repo]({{ site.sst_demo_repo }}){:target="_blank"}. So far we've been deploying our app locally through our command line. But if we had multiple people on our team, or if we were working on different features at the same time, we won't be able to work on our app because the changes would overwrite each other. @@ -40,13 +40,13 @@ Here is what our workflow is going to look like: - Built - And deployed to prod -Our workflow is fairly simple. But as your team grows, you'll need to add additionaly dev and staging environments. +Our workflow is fairly simple. But as your team grows, you'll need to add additionally dev and staging environments. ### CI/CD for Serverless -There are many common CI/CD services, like [Travis CI](https://travis-ci.org) or [CircleCI](https://circleci.com). These usually require you to manually configure the above pipeline. It involves a fair bit of scripts and configuration. +There are many common CI/CD services, like [Travis CI](https://travis-ci.org){:target="_blank"} or [CircleCI](https://circleci.com){:target="_blank"}. These usually require you to manually configure the above pipeline. It involves a fair bit of scripts and configuration. -To fix this we created a service called [**Seed**](https://seed.run). It requires no scripts and is built specifically for serverless. It also allows you to monitor and debug your serverless app. This is something we'll be doing later in the guide. +To fix this we created a service called [**Seed**](https://seed.run){:target="_blank"}. It requires no scripts and is built specifically for serverless. It also allows you to monitor and debug your serverless app. This is something we'll be doing later in the guide. We should mention that you don't have to use Seed. And this section is completely optional. diff --git a/_chapters/creating-feature-environments.md b/_chapters/creating-feature-environments.md index 317707faa..483c1aeb1 100644 --- a/_chapters/creating-feature-environments.md +++ b/_chapters/creating-feature-environments.md @@ -140,7 +140,7 @@ Select the **dev** stage, since we want the stage to be deployed into the **Deve ![Select Enable Auto-Deploy](/assets/best-practices/creating-feature-environments/select-enable-auto-deploy.png) -Click **Pipeline** to head back. +Click **Pipeline** to go. ![Head back to pipeline](/assets/best-practices/creating-feature-environments/head-back-to-pipeline.png) diff --git a/_chapters/custom-domain-in-netlify.md b/_chapters/custom-domain-in-netlify.md index dbdbe1cc7..7409de3f8 100644 --- a/_chapters/custom-domain-in-netlify.md +++ b/_chapters/custom-domain-in-netlify.md @@ -56,7 +56,7 @@ This will show you the instructions for setting up your domain through Route 53. ### DNS Settings in Route 53 -To do this we need to head back to the [AWS Console](https://console.aws.amazon.com/). and search for Route 53 as the service. +To do this we need to go to the [AWS Console](https://console.aws.amazon.com/). and search for Route 53 as the service. ![Select Route 53 service screenshot](/assets/select-route-53-service.png) diff --git a/_chapters/custom-domains-for-react-apps-on-aws.md b/_chapters/custom-domains-for-react-apps-on-aws.md index 6463489c0..3915c96f1 100644 --- a/_chapters/custom-domains-for-react-apps-on-aws.md +++ b/_chapters/custom-domains-for-react-apps-on-aws.md @@ -10,14 +10,14 @@ comments_id: custom-domains-for-react-apps-on-aws/2463 In the [previous chapter we configured a custom domain for our serverless API]({% link _chapters/custom-domains-in-serverless-apis.md %}). Now let's do the same for our frontend React.js app. -{%change%} In the `stacks/FrontendStack.js` add the following below the `new StaticSite(` line. +{%change%} In the `stacks/FrontendStack.ts` add the following below the `new StaticSite(` line. Replace the placeholders with your domain. In our examples reference **demo.my-serverless-app.com**. -```js +```typescript customDomain: app.stage === "prod" ? { - domainName: "my-serverless-app.com", - domainAlias: "www.my-serverless-app.com", + domainName: "", + domainAlias: "www.", } : undefined, ``` @@ -26,31 +26,31 @@ Just like the API case, we want to use our custom domain **if** we are deploying Of course, you can change this if you'd like to use a custom domain for the other stages. You can use something like `${app.stage}.my-serverless-app.com`. So for `dev` it'll be `dev.my-serverless-app.com`. But we'll leave this as an exercise for you. -The `domainAlias` prop is necessary because we want visitors of `www.my-serverless-app.com` to be redirected to the URL we want to use. It's a good idea to support both the `www.` and root versions of our domain. You can switch these around so that the root domain redirects to the `www.` version as well. +The `domainAlias` prop is necessary because we want visitors of `www.my-serverless-app.com` to be redirected to the URL we want to use. It's a good idea to support both the `www.` and root versions of our domain. You can switch these around so that the root domain redirects to the `www.` version as well. You won't need to set the `domainAlias` for the non-prod versions because we don't need `www.` versions for those. We need to use the custom domain URL of our API in our React app. -{%change%} Find the following line in `stacks/FrontendStack.js`. +{%change%} Find the following line in `stacks/FrontendStack.ts`. -```js +```typescript REACT_APP_API_URL: api.url, ``` {%change%} And replace it with. -```js +```typescript REACT_APP_API_URL: api.customDomainUrl || api.url, ``` -Note that, if you are going to use a custom domain locally, you might need to remove your app (`npx sst remove`) and deploy it again. This is because CDK doesn't allow you to change these references dynamically. +Note that, if you are going to use a custom domain locally, you might need to remove your app (`pnpm exec sst remove`) and deploy it again. This is because CDK doesn't allow you to change these references dynamically. We also need to update the outputs of our frontend stack. -{%change%} Replace the `stack.addOutputs` call at the bottom of `stacks/FrontendStack.js` with this. +{%change%} Replace the `stack.addOutputs` call at the bottom of `stacks/FrontendStack.ts` with this. -```js +```typescript stack.addOutputs({ SiteUrl: site.customDomainUrl || site.url || "http://localhost:3000", }); @@ -65,7 +65,7 @@ Just like the previous chapter, we need to update these changes in prod. {%change%} Run the following from your project root. ```bash -$ npx sst deploy --stage prod +$ pnpm exec sst deploy --stage prod ``` This command will take a few minutes. At the end of the deploy process you should see something like this. diff --git a/_chapters/custom-domains-in-serverless-apis.md b/_chapters/custom-domains-in-serverless-apis.md index 52b716f7a..b9d19b1ac 100644 --- a/_chapters/custom-domains-in-serverless-apis.md +++ b/_chapters/custom-domains-in-serverless-apis.md @@ -8,13 +8,19 @@ ref: custom-domains-in-serverless-apis comments_id: custom-domains-in-serverless-apis/2464 --- -In the [previous chapter]({% link _chapters/purchase-a-domain-with-route-53.md %}) we purchased a new domain on [Route 53](https://aws.amazon.com/route53/). Now let's use it for our serverless API. +In the [previous chapter]({% link _chapters/purchase-a-domain-with-route-53.md %}) we purchased a new domain on [Route 53](https://aws.amazon.com/route53/){:target="_blank"}. Now let's use it for our serverless API. -{%change%} In your `stacks/ApiStack.js` add the following above the `defaults: {` line. +{%change%} In your `stacks/ApiStack.ts` replace the function declaration with the following to add app from StackContext. -```js -customDomain: - app.stage === "prod" ? "api.my-serverless-app.com" : undefined, +```typescript +export function ApiStack({ stack, app }: StackContext) { +``` + +{%change%} Then, add the following above the `defaults: {` line in `stacks/ApiStack.ts`. +(where your actual domain would be used in place of ****) for example: **api.my-serverless-app.com**. + +```typescript +customDomain: app.stage === "prod" ? "" : undefined, ``` This tells SST that we want to use the custom domain `api.my-serverless-app.com` **if** we are deploying to the `prod` stage. We are not setting one for our `dev` stage, or any other stage. @@ -23,9 +29,9 @@ We could for example, base it on the stage name, `api-${app.stage}.my-serverless We also need to update the outputs of our API stack. -{%change%} Replace the `stack.addOutputs` call at the bottom of `stacks/ApiStack.js`. +{%change%} Finally, replace the `stack.addOutputs` call at the bottom of `stacks/ApiStack.ts`. -```js +```typescript stack.addOutputs({ ApiEndpoint: api.customDomainUrl || api.url, }); @@ -37,10 +43,10 @@ Here we are returning the custom domain URL, if we have one. If not, then we ret We are now going to deploy our app to prod. You can go ahead and stop the local development environments for SST and React. -{%change%} Run the following from your project root. +{%change%} Run the following from **your project root**. ```bash -$ npx sst deploy --stage prod +$ pnpm exec sst deploy --stage prod ``` This command will take a few minutes as it'll deploy your app to a completely new environment. Recall that we are deploying to a separate prod environment because we don't want to affect our users while we are actively developing our app. This ensures that we have a separate local dev environment and a separate prod environment. diff --git a/_chapters/delete-a-note.md b/_chapters/delete-a-note.md index 33f42b806..80ec4f77a 100644 --- a/_chapters/delete-a-note.md +++ b/_chapters/delete-a-note.md @@ -10,14 +10,14 @@ ref: delete-a-note The last thing we need to do on the note page is allowing users to delete their note. We have the button all set up already. All that needs to be done is to hook it up with the API. -{%change%} Replace our `handleDelete` function in `src/containers/Notes.js`. +{%change%} Replace our `handleDelete` function in `src/containers/Notes.tsx`. -```js +```typescript function deleteNote() { - return API.del("notes", `/notes/${id}`); + return API.del("notes", `/notes/${id}`, {}); } -async function handleDelete(event) { +async function handleDelete(event: React.FormEvent) { event.preventDefault(); const confirmed = window.confirm( diff --git a/_chapters/deploy-your-serverless-infrastructure.md b/_chapters/deploy-your-serverless-infrastructure.md index f097fdfc8..57f2e3d90 100644 --- a/_chapters/deploy-your-serverless-infrastructure.md +++ b/_chapters/deploy-your-serverless-infrastructure.md @@ -74,4 +74,4 @@ $ serverless deploy --stage prod Note that, production in this case is just an environment with a stage called `prod`. You can call it anything you like. Serverless Framework will simply create another version of your app with a completely new set of resources. You can learn more about this in our chapter on [Stages in Serverless Framework]({% link _chapters/stages-in-serverless-framework.md %}). -Next, you can head back to our main guide and [follow the frontend section]({% link _chapters/create-a-new-reactjs-app.md %}). Just remember to use the resources that were created here in your React.js app config! +Next, you can go to our main guide and [follow the frontend section]({% link _chapters/create-a-new-reactjs-app.md %}). Just remember to use the resources that were created here in your React.js app config! diff --git a/_chapters/display-a-note.md b/_chapters/display-a-note.md index b8c3aeebe..5ca0cc09c 100644 --- a/_chapters/display-a-note.md +++ b/_chapters/display-a-note.md @@ -16,9 +16,9 @@ The first thing we are going to need to do is load the note when our container l Let's add a route for the note page that we are going to create. -{%change%} Add the following line to `src/Routes.js` **below** our `/notes/new` route. +{%change%} Add the following line to `src/Routes.tsx` **below** our `/notes/new` route. -```jsx +```tsx } /> ``` @@ -28,7 +28,7 @@ By using the route path `/notes/:id` we are telling the router to send all match {%change%} And include our component in the header. -```js +```tsx import Notes from "./containers/Notes"; ``` @@ -36,9 +36,9 @@ Of course this component doesn't exist yet and we are going to create it now. ### Add the Container -{%change%} Create a new file `src/containers/Notes.js` and add the following. +{%change%} Create a new file `src/containers/Notes.tsx` and add the following. -```jsx +```tsx import React, { useRef, useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { API, Storage } from "aws-amplify"; @@ -53,7 +53,7 @@ export default function Notes() { useEffect(() => { function loadNote() { - return API.get("notes", `/notes/${id}`); + return API.get("notes", `/notes/${id}`, {}); } async function onLoad() { diff --git a/_chapters/errors-in-api-gateway.md b/_chapters/errors-in-api-gateway.md index def538490..5ac105f3f 100644 --- a/_chapters/errors-in-api-gateway.md +++ b/_chapters/errors-in-api-gateway.md @@ -21,11 +21,11 @@ Let's look at how to debug these. Head over to the `frontend/` directory in your project. -{%change%} Open `src/containers/Home.js`, and replace the `loadNotes()` function with: +{%change%} Open `src/containers/Home.tsx`, and replace the `loadNotes()` function with: -```js -function loadNotes() { - return API.get("notes", "/invalid_path"); +```tsx + function loadNotes() { + return API.get("notes", "/invalid_path", {}); } ``` @@ -77,11 +77,11 @@ This will tell you that for some reason our frontend is making a request to an i Now let's look at what happens when we use an invalid HTTP method for our API requests. Instead of a `GET` request we are going to make a `PUT` request. -{%change%} In `src/containers/Home.js` replace the `loadNotes()` function with: +{%change%} In `src/containers/Home.tsx` replace the `loadNotes()` function with: -```js +```tsx function loadNotes() { - return API.put("notes", "/notes"); + return API.put("notes", "/notes", {}); } ``` @@ -139,7 +139,7 @@ Head to the **Activity** tab in the Seed dashboard. Then click on **prod** over ![Click on prod activity in Seed](/assets/monitor-debug-errors/click-on-prod-activity-in-seed.png) -Scroll down to the last deployment from the `master` branch, past all the ones made from the `debug` branch. Hit **Rollback**. +Scroll down to the last deployment from the `main` branch, past all the ones made from the `debug` branch. Hit **Rollback**. ![Rollback on prod build in Seed](/assets/monitor-debug-errors/rollback-on-prod-build-in-seed.png) diff --git a/_chapters/errors-outside-lambda-functions.md b/_chapters/errors-outside-lambda-functions.md index 71d2e9287..e541dfa8b 100644 --- a/_chapters/errors-outside-lambda-functions.md +++ b/_chapters/errors-outside-lambda-functions.md @@ -14,21 +14,28 @@ We've covered debugging [errors in our code]({% link _chapters/logic-errors-in-l Lambda functions could fail not because of an error inside your handler code, but because of an error outside it. In this case, your Lambda function won't be invoked. Let's add some faulty code outside our handler function. -{%change%} Replace the `main` function in `packages/functions/src/get.js` with the following. +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. -```js +```typescript // Some faulty code dynamoDb.notExist(); -export const main = handler(async (event) => { +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved - // - 'userId': Identity Pool identity id of the authenticated user - // - 'noteId': path parameter + // 'Key' defines the partition key and sort key of + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - noteId: event.pathParameters.id, + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path }, }; @@ -40,6 +47,7 @@ export const main = handler(async (event) => { // Return the retrieved item return result.Item; }); + ``` {%change%} Commit this code. @@ -68,23 +76,30 @@ Note that, you might see there are 3 events for this error. This is because the Another error that can happen outside a Lambda function is when the handler has been misnamed. -{%change%} Replace the `main` function in `packages/functions/src/get.js` with the following. +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. -```js +```typescript // Wrong handler function name -export const main2 = handler(async (event) => { +export const main2 = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved - // - 'userId': Identity Pool identity id of the authenticated user - // - 'noteId': path parameter + // 'Key' defines the partition key and sort key of + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - noteId: event.pathParameters.id, + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path }, }; - const result = await dynamoDbLib.call("get", params); + const result = await dynamoDb.get(params); if (!result.Item) { throw new Error("Item not found."); } @@ -92,6 +107,7 @@ export const main2 = handler(async (event) => { // Return the retrieved item return result.Item; }); + ``` {%change%} Let's commit this. @@ -116,20 +132,30 @@ And that about covers the main Lambda function errors. So the next time you see Let's cleanup all the faulty code. -{%change%} Replace `packages/functions/src/get.js` with the following. +{%change%} Replace `packages/functions/src/get.ts` with the following. -```js -import { Table } from "sst/node/table"; +```typescript import handler from "@notes/core/handler"; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { Table } from "sst/node/table"; import dynamoDb from "@notes/core/dynamodb"; -export const main = handler(async (event) => { +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved + // 'Key' defines the partition key and sort key of + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - noteId: event.pathParameters.id, // The id of the note from the path + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path }, }; @@ -141,6 +167,7 @@ export const main = handler(async (event) => { // Return the retrieved item return result.Item; }); + ``` Commit and push the code. diff --git a/_chapters/getting-production-ready.md b/_chapters/getting-production-ready.md index 582ebd3c6..acb98b2a8 100644 --- a/_chapters/getting-production-ready.md +++ b/_chapters/getting-production-ready.md @@ -14,7 +14,7 @@ Over the next few chapters we will look at how to get your app ready for product - **Automating deployments** - So far you've had to deploy through your command line using the `npx sst deploy` command. When you have a team working on your project, you want to make sure the deployments to production are centralized. This ensures that you have control over what gets deployed to production. We'll go over how to automate your deployments using [Seed](https://seed.run). + So far you've had to deploy through your command line using the `pnpm exec sst deploy` command. When you have a team working on your project, you want to make sure the deployments to production are centralized. This ensures that you have control over what gets deployed to production. We'll go over how to automate your deployments using [Seed](https://seed.run){:target="_blank"}. - **Monitoring and debugging errors in production** diff --git a/_chapters/give-feedback-while-logging-in.md b/_chapters/give-feedback-while-logging-in.md index f35c29034..d8c37caec 100644 --- a/_chapters/give-feedback-while-logging-in.md +++ b/_chapters/give-feedback-while-logging-in.md @@ -12,16 +12,16 @@ It's important that we give the user some feedback while we are logging them in. ### Use an isLoading Flag -{%change%} To do this we are going to add an `isLoading` flag to the state of our `src/containers/Login.js`. Add the following to the top of our `Login` function component. +{%change%} To do this we are going to add an `isLoading` flag to the state of our `src/containers/Login.tsx`. Add the following to the top of our `Login` function component. -```js +```tsx const [isLoading, setIsLoading] = useState(false); ``` {%change%} And we'll update it while we are logging in. So our `handleSubmit` function now looks like so: -```js -async function handleSubmit(event) { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setIsLoading(true); @@ -30,9 +30,14 @@ async function handleSubmit(event) { await Auth.signIn(email, password); userHasAuthenticated(true); nav("/"); - } catch (e) { - alert(e.message); - setIsLoading(false); + } catch (error) { + // Prints the full error + console.error(error); + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } } } ``` @@ -49,20 +54,21 @@ $ mkdir src/components/ Here we'll be storing all our React components that are not dealing directly with our API or responding to routes. -{%change%} Create a new file and add the following in `src/components/LoaderButton.js`. +{%change%} Create a new file and add the following in `src/components/LoaderButton.tsx`. -```jsx +```tsx import React from "react"; import Button from "react-bootstrap/Button"; -import { BsArrowRepeat } from "react-icons/bs"; +import {BsArrowRepeat} from "react-icons/bs"; import "./LoaderButton.css"; + export default function LoaderButton({ - isLoading, - className = "", - disabled = false, - ...props -}) { + isLoading = false, + className = "", + disabled = false, + ...props + }) { return ( ``` @@ -137,13 +144,13 @@ Now we can use our new component in our `Login` container. {%change%} Also, let's replace `Button` import in the header. Remove this. -```js +```tsx import Button from "react-bootstrap/Button"; ``` {%change%} And add the following. -```js +```tsx import LoaderButton from "../components/LoaderButton"; ``` @@ -155,53 +162,69 @@ And now when we switch over to the browser and try logging in, you should see th You might have noticed in our Login and App components that we simply `alert` when there is an error. We are going to keep our error handling simple. But it'll help us further down the line if we handle all of our errors in one place. -{%change%} To do that, create `src/lib/errorLib.js` and add the following. - -```js -export function onError(error) { - let message = error.toString(); +{%change%} To do that, create `src/lib/errorLib.ts` and add the following. - // Auth errors - if (!(error instanceof Error) && error.message) { - message = error.message; +```typescript +export function onError(error: unknown) { + if (error !== "No current user") { + return; + } + + let message = String(error); + + if (!(error instanceof Error) + && error + && typeof error === 'object' + && 'message' in error + && error.message) { + message = String(error.message); } alert(message); } + + ``` The `Auth` package throws errors in a different format, so all this code does is `alert` the error message we need. And in all other cases simply `alert` the error object itself. -Let's use this in our Login container. +Let's use this in our Login container (containers/Login.tsx). -{%change%} Import the new error lib in the header of `src/containers/Login.js`. +{%change%} Replace the catch statement in the `handleSubmit` function with: -```js -import { onError } from "../lib/errorLib"; +```tsx +catch (error: unknown) { + onError(error); +} ``` -{%change%} And replace `alert(e.message);` in the `handleSubmit` function with: +{%change%} And import the new error lib in the header of `src/containers/Login.tsx`. -```js -onError(e); +```tsx +import { onError } from "../lib/errorLib"; ``` + We'll do something similar in the App component. -{%change%} Import the error lib in the header of `src/App.js`. +{%change%} Replace the catch statement in the `onLoad` function with: -```js -import { onError } from "./lib/errorLib"; +```tsx +catch (error: unknown) { + onError(error); +} ``` -{%change%} And replace `alert(e);` in the `onLoad` function with: +{%change%} And import the error lib in the header of `src/App.tsx`. -```js -onError(e); +```tsx +import { onError } from "./lib/errorLib"; ``` -We'll improve our error handling a little later on in the guide. -Also, if you would like to add _Forgot Password_ functionality for your users, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}). +We'll improve our error handling a little later on in the guide. +{%aside%} +Also, if you would like to add _Forgot Password_ functionality for your users, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}){:target="_blank"}. +{%endaside%} For now, we are ready to move on to the sign up process for our app. diff --git a/_chapters/handle-404s.md b/_chapters/handle-404s.md index 4557efef7..749688d2c 100644 --- a/_chapters/handle-404s.md +++ b/_chapters/handle-404s.md @@ -14,9 +14,9 @@ Now that we know how to handle the basic routes; let's look at handling 404s wit Let's start by creating a component that will handle this for us. -{%change%} Create a new component at `src/containers/NotFound.js` and add the following. +{%change%} Create a new component at `src/containers/NotFound.tsx` and add the following. -```jsx +```tsx import React from "react"; import "./NotFound.css"; @@ -43,9 +43,9 @@ All this component does is print out a simple message for us. Now we just need to add this component to our routes to handle our 404s. -{%change%} Find the `` block in `src/Routes.js` and add it as the last line in that section. +{%change%} Find the `` block in `src/Routes.tsx` and add it as the last line in that section. -```jsx +```tsx { /* Finally, catch all unmatched routes */ } diff --git a/_chapters/handle-cors-in-s3-for-file-uploads.md b/_chapters/handle-cors-in-s3-for-file-uploads.md index 884b656b1..92a0c485a 100644 --- a/_chapters/handle-cors-in-s3-for-file-uploads.md +++ b/_chapters/handle-cors-in-s3-for-file-uploads.md @@ -8,19 +8,19 @@ description: In this chapter we'll look at how to configure CORS for an S3 bucke comments_id: handle-cors-in-s3-for-file-uploads/2174 --- -In the notes app we are building, users will be uploading files to the bucket we just created. And since our app will be served through our custom domain, it'll be communicating across domains while it does the uploads. By default, S3 does not allow its resources to be accessed from a different domain. However, [cross-origin resource sharing (CORS)](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) defines a way for client web applications that are loaded in one domain to interact with resources in a different domain. +In the notes app we are building, users will be uploading files to the bucket we just created. And since our app will be served through our custom domain, it'll be communicating across domains while it does the uploads. By default, S3 does not allow its resources to be accessed from a different domain. However, [cross-origin resource sharing (CORS)](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"} defines a way for client web applications that are loaded in one domain to interact with resources in a different domain. Let's enable CORS for our S3 bucket. -{%change%} Replace the following line in `stacks/StorageStack.js`. +{%change%} Replace the following line in `stacks/StorageStack.ts`. -```js +```typescript const bucket = new Bucket(stack, "Uploads"); ``` {%change%} With this. -```js +```typescript const bucket = new Bucket(stack, "Uploads", { cors: [ { diff --git a/_chapters/handle-cors-in-serverless-apis.md b/_chapters/handle-cors-in-serverless-apis.md index e9363a3dd..aafd2b5cc 100644 --- a/_chapters/handle-cors-in-serverless-apis.md +++ b/_chapters/handle-cors-in-serverless-apis.md @@ -10,7 +10,7 @@ comments_id: handle-cors-in-serverless-apis/2175 Let's take stock of our setup so far. We have a serverless API backend that allows users to create notes and an S3 bucket where they can upload files. We are now almost ready to work on our frontend React app. -However, before we can do that. There is one thing that needs to be taken care of — [CORS or Cross-Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). +However, before we can do that. There is one thing that needs to be taken care of — [CORS or Cross-Origin Resource Sharing](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"}. Since our React app is going to be run inside a browser (and most likely hosted on a domain separate from our serverless API and S3 bucket), we need to configure CORS to allow it to connect to our resources. @@ -34,7 +34,7 @@ There are two things we need to do to support CORS in our serverless API. For all the other types of requests we need to make sure to include the appropriate CORS headers. These headers, just like the one above, need to include the domains that are allowed. -There's a bit more to CORS than what we have covered here. So make sure to [check out the Wikipedia article for further details](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing). +There's a bit more to CORS than what we have covered here. So make sure to [check out the Wikipedia article for further details](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing){:target="_blank"}. If we don't set the above up, then we'll see something like this in our HTTP responses. @@ -46,9 +46,9 @@ And our browser won't show us the HTTP response. This can make debugging our API ### CORS in API Gateway -The SST [`Api`]({{ site.docs_url }}/constructs/Api) construct that we are using enables CORS by default. +The SST [`Api`]({{ site.docs_url }}/constructs/Api){:target="_blank"} construct that we are using enables CORS by default. -```js +```typescript new Api(this, "Api", { // Enabled by default cors: true, @@ -58,9 +58,9 @@ new Api(this, "Api", { }); ``` -You can further configure the specifics if necessary. You can [read more about this here]({{ site.docs_url }}/constructs/Api#cors). +You can further configure the specifics if necessary. You can [read more about this here]({{ site.docs_url }}/constructs/Api#cors){:target="_blank"}. -```js +```typescript new Api(this, "Api", { cors: { allowMethods: ["get"], @@ -77,9 +77,9 @@ We'll go with the default setting for now. Next, we need to add the CORS headers in our Lambda function response. -{%change%} Replace the `return` statement in our `packages/core/src/handler.js`. +{%change%} Replace the `return` statement in our `packages/core/src/handler.ts`. -```js +```typescript return { statusCode, body: JSON.stringify(body), @@ -88,7 +88,7 @@ return { {%change%} With the following. -```js +```typescript return { statusCode, body: JSON.stringify(body), diff --git a/_chapters/handle-routes-with-react-router.md b/_chapters/handle-routes-with-react-router.md index 12a510765..9980cf87f 100644 --- a/_chapters/handle-routes-with-react-router.md +++ b/_chapters/handle-routes-with-react-router.md @@ -16,21 +16,21 @@ Let's start by installing React Router. ### Installing React Router -{%change%} Run the following command in the `frontend/` directory and **not** in your project root. +{%change%} Run the following command **in the `frontend/` directory** and **not** in your project root. ```bash -$ npm install react-router-dom +$ pnpm add --save react-router-dom ``` -This installs the NPM package and adds the dependency to the `package.json` of your React app. +This installs the package and adds the dependency to `package.json` in your React app. ### Setting up React Router -Even though we don't have any routes set up in our app, we can get the basic structure up and running. Our app currently runs from the `App` component in `src/App.js`. We are going to be using this component as the container for our entire app. To do that we'll encapsulate our `App` component within a `Router`. +Even though we don't have any routes set up in our app, we can get the basic structure up and running. Our app currently runs from the `App` component in `src/App.tsx`. We are going to be using this component as the container for our entire app. To do that we'll encapsulate our `App` component within a `Router`. -{%change%} Replace the following code in `src/index.js`: +{%change%} Replace the following code in `src/index.tsx`: -```jsx +```tsx root.render( @@ -40,7 +40,7 @@ root.render( {%change%} With this: -```jsx +```tsx root.render( @@ -50,9 +50,9 @@ root.render( ); ``` -{%change%} And import this in the header of `src/index.js`. +{%change%} And import this in the header of `src/index.tsx`. -```jsx +```tsx import { BrowserRouter as Router } from "react-router-dom"; ``` diff --git a/_chapters/handling-secrets-in-sst.md b/_chapters/handling-secrets-in-sst.md index d73ffe761..e15340757 100644 --- a/_chapters/handling-secrets-in-sst.md +++ b/_chapters/handling-secrets-in-sst.md @@ -10,50 +10,50 @@ comments_id: handling-secrets-in-sst/2465 In the [previous chapter]({% link _chapters/setup-a-stripe-account.md %}), we created a Stripe account and got a pair of keys. Including the Stripe secret key. We need this in our app but we do not want to store this secret environment variables in our code. In this chapter, we'll look at how to add secrets in SST. -We are going to create a `.env` file to store this. +We will be using the [SST CLI]({{ site.docs_url }}/packages/sst){:target="_blank"} to [store secrets]({{ site.docs_url }}/packages/sst#sst-secrets){:target="_blank"} in the [AWS SSM Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html){:target="_blank"}. -{%change%} Create a new file in `.env.local` with the following. +{%change%} run the following in your terminal: ```bash -STRIPE_SECRET_KEY=STRIPE_TEST_SECRET_KEY +$ pnpm exec sst secrets set --fallback STRIPE_SECRET_KEY ``` -Make sure to replace the `STRIPE_TEST_SECRET_KEY` with the **Secret key** from the [previous]({% link _chapters/setup-a-stripe-account.md %}) chapter. +{%note%} +You can specify the stage for the secret. By default, the stage is your local stage. +{%endnote%} -SST automatically loads this into your application. +You can run `pnpm exec sst secrets list` to see the secrets for the current stage. -A note on committing these files. SST follows the convention used by [Create React App](https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env) and [others](https://nextjs.org/docs/basic-features/environment-variables#default-environment-variables) of committing `.env` files to Git but not the `.env.local` or `.env.$STAGE.local` files. You can [read more about it here]({{ site.docs_url }}/config#committing-env-files). +Now that the secret is stored in AWS Parameter Store, we can add it into our stack using the [Config helper]({{ site.docs_url }}/config#define-a-secret){:target="_blank"}. -To ensure that this file doesn't get committed, we'll need to add it to the `.gitignore` in our project root. You'll notice that the starter project we are using already has this in the `.gitignore`. +{%change%} Add the following within the ApiStack function in `stacks/ApiStack.ts`: -```txt -# environments -.env*.local +```typescript + const STRIPE_SECRET_KEY = new Config.Secret(stack, "STRIPE_SECRET_KEY"); ``` -Also, since we won't be committing this file to Git, we'll need to add this to our CI when we want to automate our deployments. We'll do this later in the guide. +{%change%} Import `Config` in `stacks/ApiStack.js`: -Next, let's add these to our functions. - -{%change%} Add the following below the `bind: [table],` line in `stacks/ApiStack.js`: - -```js -environment: { - STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, -}, +```typescript +import {Config} from "sst/constructs"; ``` -We are taking the environment variables in our SST app and passing it into our API. - -### Deploy our changes +{%change%} Bind `STRIPE_SECRET_KEY` into the api defaults in `stacks/ApiStack.ts`. -Switch over to your terminal and restart `sst dev` so that it picks up the new `.env.local` file. +Replace: +```typescript + function: { + bind: [table], + }, +``` -```bash -✓ Deployed: - StorageStack - ApiStack - ... +with: +```typescript + function: { + bind: [table, STRIPE_SECRET_KEY], + }, ``` +This will add `STRIPE_SECRET_KEY` as a secret in the stack. We can now use it in our Lambda function. + Now we are ready to add an API to handle billing. diff --git a/_chapters/initialize-a-github-repo.md b/_chapters/initialize-a-github-repo.md index f0a255fac..b9e4df12e 100644 --- a/_chapters/initialize-a-github-repo.md +++ b/_chapters/initialize-a-github-repo.md @@ -27,7 +27,7 @@ Once your repository is created, copy the repository URL. We'll need this soon. ![Copy new GitHub repo url screenshot](/assets/part2/copy-new-github-repo-url.png) -In our case the URL is: +For example, [the demo code repository URL](https://github.com/serverless-stack/demo-notes-app.git){:target="_blank"} is: ``` txt https://github.com/sst/demo-notes-app.git @@ -35,7 +35,7 @@ https://github.com/sst/demo-notes-app.git ### Initialize Your New Repo -{%change%} Now head back to your project and use the following command to initialize your new repo. +{%change%} Now go to your project and use the following command to initialize your new repo. ``` bash $ git init @@ -45,27 +45,29 @@ $ git init ``` bash $ git add . -``` +``` {%change%} Create your first commit. ``` bash -$ git commit -m "First commit" +$ git commit -m "First commit" ``` -{%change%} Link it to the repo you created on GitHub. +{%change%} Link it to the repo you created on GitHub by running the following, replacing **** with your repository URL. ``` bash $ git branch -M main -$ git remote add origin REPO_URL +$ git remote add origin ``` - +{%aside%} Here `REPO_URL` is the URL we copied from GitHub in the steps above. You can verify that it has been set correctly by doing the following. ``` bash $ git remote -v ``` +{%endaside%} + {%change%} Finally, let's push our first commit to GitHub using: ``` bash diff --git a/_chapters/list-all-the-notes.md b/_chapters/list-all-the-notes.md index 3088c01c6..1927e9c63 100644 --- a/_chapters/list-all-the-notes.md +++ b/_chapters/list-all-the-notes.md @@ -12,13 +12,12 @@ Now that we are able to create a new note, let's create a page where we can see Currently, our Home container is very simple. Let's add the conditional rendering in there. -{%change%} Replace our `src/containers/Home.js` with the following. +{%change%} Replace our `src/containers/Home.tsx` with the following. -```jsx -import React, { useState, useEffect } from "react"; +```tsx +import React, {useState} from "react"; import ListGroup from "react-bootstrap/ListGroup"; -import { useAppContext } from "../lib/contextLib"; -import { onError } from "../lib/errorLib"; +import {useAppContext} from "../lib/contextLib"; import "./Home.css"; export default function Home() { @@ -26,7 +25,9 @@ export default function Home() { const { isAuthenticated } = useAppContext(); const [isLoading, setIsLoading] = useState(true); - function renderNotesList(notes) { + function renderNotesList(notes: { + [key: string | symbol]: any; + }) { return null; } @@ -54,13 +55,14 @@ export default function Home() {
); } + ``` We are doing a few things of note here: 1. Rendering the lander or the list of notes based on `isAuthenticated` flag in our app context. - ```js + ```tsx { isAuthenticated ? renderNotes() : renderLander(); } @@ -68,9 +70,9 @@ We are doing a few things of note here: 2. Store our notes in the state. Currently, it's empty but we'll be calling our API for it. -3. Once we fetch our list we'll use the `renderNotesList` method to render the items in the list. +3. Once we fetch our list we'll use the `renderNotesList` method to render the items in the list. We temporarily added a generic object definition for the notes Type. -4. We're using the [Bootstrap utility classes](https://getbootstrap.com/docs/4.5/utilities/spacing/) `pb-3` (padding bottom), `mt-4` (margin top), `mb-3` (margin bottom), and `border-bottom` to style the _Your Notes_ header. +4. We're using the [Bootstrap utility classes](https://getbootstrap.com/docs/4.5/utilities/spacing/){:target="_blank"} `pb-3` (padding bottom), `mt-4` (margin top), `mb-3` (margin bottom), and `border-bottom` to style the _Your Notes_ header. And that's our basic setup! Head over to the browser and the homepage of our app should render out an empty list. diff --git a/_chapters/load-the-state-from-the-session.md b/_chapters/load-the-state-from-the-session.md index cf910ab92..809ab14f8 100644 --- a/_chapters/load-the-state-from-the-session.md +++ b/_chapters/load-the-state-from-the-session.md @@ -14,41 +14,47 @@ Amplify gives us a way to get the current user session using the `Auth.currentSe ### Load User Session -Let's load this when our app loads. To do this we are going to use another React hook, called [useEffect](https://reactjs.org/docs/hooks-effect.html). Since `Auth.currentSession()` returns a promise, it means that we need to ensure that the rest of our app is only ready to go after this has been loaded. +Let's load this when our app loads. To do this we are going to use another React hook, called [useEffect](https://reactjs.org/docs/hooks-effect.html){:target="_blank"}. Since `Auth.currentSession()` returns a promise, it means that we need to ensure that the rest of our app is only ready to go after this has been loaded. -{%change%} To do this, let's add another state variable to our `src/App.js` state called `isAuthenticating`. Add it to the top of our `App` function. +{%change%} To do this, let's add another state variable to our `src/App.tsx` state called `isAuthenticating`. Add it to the top of our `App` function. -```js -const [isAuthenticating, setIsAuthenticating] = useState(true); +```tsx +const [isAuthenticating, setIsAuthenticating] = useState(true); ``` We start with the value set to `true` because as we first load our app, it'll start by checking the current authentication state. -{%change%} Let's include the `Auth` module by adding the following to the header of `src/App.js`. +{%change%} To load the user session we'll add the following to our `src/App.tsx` right below our variable declarations. -```js -import { Auth } from "aws-amplify"; -``` - -{%change%} Now to load the user session we'll add the following to our `src/App.js` right below our variable declarations. - -```js -useEffect(() => { - onLoad(); +```tsx + useEffect(() => { + onLoad(); }, []); async function onLoad() { - try { - await Auth.currentSession(); - userHasAuthenticated(true); - } catch (e) { - if (e !== "No current user") { - alert(e); + try { + await Auth.currentSession(); + userHasAuthenticated(true); + } catch (e) { + if (e !== "No current user") { + alert(e); + } } - } - setIsAuthenticating(false); + setIsAuthenticating(false); } + +``` +{%change%} Then include the `Auth` module by adding the following to the header of `src/App.tsx`. + +```tsx +import { Auth } from "aws-amplify"; +``` + +{%change%} Let's make sure to include the `useEffect` hook by replacing the React import in the header of `src/App.tsx` with: + +```tsx +import React, { useEffect, useState } from "react"; ``` Let's understand how this and the `useEffect` hook works. @@ -65,22 +71,16 @@ When our app first loads, it'll run the `onLoad` function. All this does is load So the top of our `App` function should now look like this: -```js +```tsx function App() { - const [isAuthenticating, setIsAuthenticating] = useState(true); - const [isAuthenticated, userHasAuthenticated] = useState(false); - - useEffect(() => { - onLoad(); - }, []); - - ... -``` - -{%change%} Let's make sure to include the `useEffect` hook by replacing the React import in the header of `src/App.js` with: - -```js -import React, { useState, useEffect } from "react"; + const [isAuthenticating, setIsAuthenticating] = useState(true); + const [isAuthenticated, userHasAuthenticated] = useState(false); + + useEffect(() => { + onLoad(); + }, []); + + ... ``` ### Render When the State Is Ready @@ -89,13 +89,13 @@ Since loading the user session is an asynchronous process, we want to ensure tha We'll conditionally render our app based on the `isAuthenticating` flag. -{%change%} Our `return` statement in `src/App.js` should be as follows. +{%change%} Replace our `return` statement in `src/App.tsx` with the following functions which are now called in the return. {% raw %} -```jsx -return ( - !isAuthenticating && ( +```tsx +function authenticationComplete() { + return (
@@ -119,12 +119,22 @@ return ( - +
) -); +} + +function authenticationInProgress() { + return ( +
Wait...
+ ) +} + +return ( + isAuthenticating ? authenticationInProgress() : authenticationComplete() +) ``` {% endraw %} diff --git a/_chapters/logic-errors-in-lambda-functions.md b/_chapters/logic-errors-in-lambda-functions.md index 82334264c..58b4309ac 100644 --- a/_chapters/logic-errors-in-lambda-functions.md +++ b/_chapters/logic-errors-in-lambda-functions.md @@ -24,18 +24,26 @@ $ git checkout -b debug ### Push Some Faulty Code -Let's trigger an error in `get.js` by commenting out the `noteId` field in the DynamoDB call's Key definition. This will cause the DynamoDB call to fail and in turn cause the Lambda function to fail. +Let's trigger an error in `get.ts` by commenting out the `noteId` field in the DynamoDB call's Key definition. This will cause the DynamoDB call to fail and in turn cause the Lambda function to fail. -{%change%} Replace the `main` function in `packages/functions/src/get.js` with the following. +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } -```js -export const main = handler(async (event) => { const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved + // 'Key' defines the partition key and sort key of0 + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - // noteId: event.pathParameters.id, // The id of the note from the path + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, // The id of the author + // noteId: path_id, // The id of the note from the path }, }; @@ -56,7 +64,7 @@ Note the line that we've commented out. ```bash $ git add . $ git commit -m "Adding some faulty code" -$ git push --set-upstream origin debug +$ git push --set-upstream origin debug; ``` ### Deploy the Faulty Code diff --git a/_chapters/login-with-aws-cognito.md b/_chapters/login-with-aws-cognito.md index df4690e40..3dd22cf9e 100644 --- a/_chapters/login-with-aws-cognito.md +++ b/_chapters/login-with-aws-cognito.md @@ -10,33 +10,38 @@ comments_id: login-with-aws-cognito/129 We are going to use AWS Amplify to login to our Amazon Cognito setup. Let's start by importing it. -### Import Auth from AWS Amplify - -{%change%} Add the Auth module to the header of our Login container in `src/containers/Login.js`. - -```jsx -import { Auth } from "aws-amplify"; -``` - ### Login to Amazon Cognito The login code itself is relatively simple. -{%change%} Simply replace our placeholder `handleSubmit` method in `src/containers/Login.js` with the following. +{%change%} Simply replace our placeholder `handleSubmit` method in `src/containers/Login.tsx` with the following. + +```tsx +async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + + try { + await Auth.signIn(email, password); + alert("Logged in"); + } catch (error) { + // Prints the full error + console.error(error); + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } + } +} +``` -```js -async function handleSubmit(event) { - event.preventDefault(); +{%change%} And add the Auth module to the header of our Login container in `src/containers/Login.tsx`. - try { - await Auth.signIn(email, password); - alert("Logged in"); - } catch (e) { - alert(e.message); - } -} +```tsx +import { Auth } from "aws-amplify"; ``` + We are doing two things of note here. 1. We grab the `email` and `password` and call Amplify's `Auth.signIn()` method. This method returns a promise since it will be logging in the user asynchronously. diff --git a/_chapters/redirect-on-login-and-logout.md b/_chapters/redirect-on-login-and-logout.md index ce3913cc4..a0c1bc84c 100644 --- a/_chapters/redirect-on-login-and-logout.md +++ b/_chapters/redirect-on-login-and-logout.md @@ -13,37 +13,43 @@ To complete the login flow we are going to need to do two more things. 1. Redirect the user to the homepage after they login. 2. And redirect them back to the login page after they logout. -We are going to use the `useNavigate` hook that comes with React Router. This will allow us to use the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History). +We are going to use the `useNavigate` hook that comes with React Router. This will allow us to use the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History){:target="_blank"}. ### Redirect to Home on Login -{%change%} First, initialize `useNavigate` hook in the beginning of `src/containers/Login.js`. +{%change%} First, initialize `useNavigate` hook in the beginning of `src/containers/Login.tsx`. -```js +```tsx const nav = useNavigate(); ``` Make sure to add it below the `export default function Login() {` line. -{%change%} Then update the `handleSubmit` method in `src/containers/Login.js` to look like this: +{%change%} Then update the `handleSubmit` method in `src/containers/Login.tsx` to look like this: -```js -async function handleSubmit(event) { +```tsx + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); try { await Auth.signIn(email, password); userHasAuthenticated(true); nav("/"); - } catch (e) { - alert(e.message); + } catch (error) { + // Prints the full error + console.error(error); + if (error instanceof Error) { + alert(error.message); + } else { + alert(String(error)); + } } } ``` -{%change%} Also, import `useNavigate` from React Router in the header of `src/containers/Login.js`. +{%change%} Also, import `useNavigate` from React Router in the header of `src/containers/Login.tsx`. -```js +```tsx import { useNavigate } from "react-router-dom"; ``` @@ -55,27 +61,27 @@ Now if you head over to your browser and try logging in, you should be redirecte Now we'll do something very similar for the logout process. -{%change%} Add the `useNavigate` hook in the beginning of `App` component. +{%change%} Add the `useNavigate` hook in the beginning of `App` component in `src/App.tsx`. -```js +```tsx const nav = useNavigate(); ``` -{%change%} Import `useNavigate` from React Router in the header of `src/App.js`. +{%change%} Import `useNavigate` from React Router in the header of `src/App.tsx`. -```js +```tsx import { useNavigate } from "react-router-dom"; ``` -{%change%} Add the following to the bottom of the `handleLogout` function in our `src/App.js`. +{%change%} Add the following to the bottom of the `handleLogout` function in our `src/App.tsx`. -```jsx +```tsx nav("/login"); ``` So our `handleLogout` function should now look like this. -```js +```tsx async function handleLogout() { await Auth.signOut(); diff --git a/_chapters/redirect-on-login.md b/_chapters/redirect-on-login.md index 8812c559c..4519c96f5 100644 --- a/_chapters/redirect-on-login.md +++ b/_chapters/redirect-on-login.md @@ -12,10 +12,10 @@ Our secured pages redirect to the login page when the user is not logged in, wit Let's start by adding a method to read the `redirect` URL from the querystring. -{%change%} Add the following method to your `src/components/UnauthenticatedRoute.js` below the imports. +{%change%} Add the following method to your `src/components/UnauthenticatedRoute.tsx` below the imports and interface. -```jsx -function querystring(name, url = window.location.href) { +```tsx +function querystring(name: string, url = window.location.href) { const parsedName = name.replace(/[[]]/g, "\\$&"); const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`, "i"); const results = regex.exec(url); @@ -34,8 +34,8 @@ Now let's update our component to use this parameter when it redirects. {%change%} Replace our current `UnauthenticatedRoute` function component with the following. -```jsx -export default function UnauthenticatedRoute(props) { +```tsx +export default function UnauthenticatedRoute(props: AuthProps) { const { isAuthenticated } = useAppContext(); const { children } = props; const redirect = querystring("redirect"); @@ -48,15 +48,15 @@ export default function UnauthenticatedRoute(props) { } ``` -{%change%} And remove the following from the `handleSubmit` method in `src/containers/Login.js`. +{%change%} And remove the following from the `handleSubmit` method in `src/containers/Login.tsx`. -```jsx +```tsx nav("/"); ``` {%change%} Also, remove the hook declaration. -```jsx +```tsx const nav = useNavigate(); ``` diff --git a/_chapters/render-the-note-form.md b/_chapters/render-the-note-form.md index b5eee1eac..331798140 100644 --- a/_chapters/render-the-note-form.md +++ b/_chapters/render-the-note-form.md @@ -10,22 +10,23 @@ ref: render-the-note-form Now that our container loads a note using the `useEffect` method, let's go ahead and render the form that we'll use to edit it. -{%change%} Replace our placeholder `return` statement in `src/containers/Notes.js` with the following. +{%change%} Replace our placeholder `return` statement in `src/containers/Notes.tsx` with the following. -```jsx +```tsx function validateForm() { return content.length > 0; } -function formatFilename(str) { +function formatFilename(str: string) { return str.replace(/^\w+-/, ""); } -function handleFileChange(event) { - file.current = event.target.files[0]; +function handleFileChange(event: React.ChangeEvent) { + if ( event.currentTarget.files === null ) return + file.current = event.currentTarget.files[0]; } -async function handleSubmit(event) { +async function handleSubmit(event: React.FormEvent) { let attachment; event.preventDefault(); @@ -42,7 +43,7 @@ async function handleSubmit(event) { setIsLoading(true); } -async function handleDelete(event) { +async function handleDelete(event: React.FormEvent) { event.preventDefault(); const confirmed = window.confirm( @@ -105,28 +106,22 @@ return (
); ``` +{%change%} To complete this code, Let's add `isLoading` and `isDeleting` below the state and ref declarations at the top of our `Notes` component function. -We are doing a few things here: - -1. We render our form only when the `note` state variable is set. - -2. Inside the form we conditionally render the part where we display the attachment by using `note.attachment`. - -3. We format the attachment URL using `formatFilename` by stripping the timestamp we had added to the filename while uploading it. - -4. We also added a delete button to allow users to delete the note. And just like the submit button it too needs a flag that signals that the call is in progress. We call it `isDeleting`. - -5. We handle attachments with a file input exactly like we did in the `NewNote` component. - -6. Our delete button also confirms with the user if they want to delete the note using the browser's `confirm` dialog. +```tsx +const [isLoading, setIsLoading] = useState(false); +const [isDeleting, setIsDeleting] = useState(false); +``` +{%change%} Then replace the `const file` definition with the following -To complete this code, let's add `isLoading` and `isDeleting` to the state. +```tsx +const file = useRef(null); +``` -{%change%} Add these below the state and ref declarations at the top of our `Notes` component function. +{%change%} as well as the `const note/setNote` definition as follows: -```js -const [isLoading, setIsLoading] = useState(false); -const [isDeleting, setIsDeleting] = useState(false); +```tsx +const [note, setNote] = useState(null); ``` {%change%} Let's also add some styles by adding the following to `src/containers/Notes.css`. @@ -138,15 +133,31 @@ const [isDeleting, setIsDeleting] = useState(false); } ``` -{%change%} Also, let's include the React-Bootstrap components that we are using here by adding the following to our header. And our styles, the `LoaderButton`, and the `config`. +{%change%} and finally, let's include the React-Bootstrap components that we are using here by adding the following to our header. And our styles, the `LoaderButton`, and the `config`. ```js import Form from "react-bootstrap/Form"; import LoaderButton from "../components/LoaderButton"; import config from "../config"; +import {NotesType} from "../lib/notesLib"; import "./Notes.css"; ``` +We are doing a few things here: + +1. We render our form only when the `note` state variable is set. + +2. Inside the form we conditionally render the part where we display the attachment by using `note.attachment`. + +3. We format the attachment URL using `formatFilename` by stripping the timestamp we had added to the filename while uploading it. + +4. We also added a delete button to allow users to delete the note. And just like the submit button it too needs a flag that signals that the call is in progress. We call it `isDeleting`. + +5. We handle attachments with a file input exactly like we did in the `NewNote` component. + +6. Our delete button also confirms with the user if they want to delete the note using the browser's `confirm` dialog. + + And that's it. If you switch over to your browser, you should see the note loaded. ![Notes page loaded screenshot](/assets/notes-page-loaded.png) diff --git a/_chapters/report-api-errors-in-react.md b/_chapters/report-api-errors-in-react.md index e3a3e3232..4c3cc10de 100644 --- a/_chapters/report-api-errors-in-react.md +++ b/_chapters/report-api-errors-in-react.md @@ -10,9 +10,9 @@ ref: report-api-errors-in-react Now that we have our [React app configured with Sentry]({% link _chapters/setup-error-reporting-in-react.md %}), let's go ahead and start sending it some errors. -So far we've been using the `onError` method in `src/lib/errorLib.js` to handle errors. Recall that it doesn't do a whole lot outside of alerting the error. +So far we've been using the `onError` method in `src/lib/errorLib.ts` to handle errors. Recall that it doesn't do a whole lot outside of alerting the error. -```js +```typescript export function onError(error) { let message = error.toString(); @@ -29,24 +29,58 @@ For most errors we simply alert the error message. But Amplify's Auth package do For API errors we want to report both the error and the API endpoint that caused the error. On the other hand, for Auth errors we need to create an `Error` object because Sentry needs actual errors sent to it. -{%change%} Replace the `onError` method in `src/lib/errorLib.js` with the following: +{%change%} Replace the `onError` method in `src/lib/errorLib.ts` with the following: -```js -export function onError(error) { - let errorInfo = {}; - let message = error.toString(); +```typescript +export function onError(error: any) { + if (error === "No current user") { + // discard auth errors from non-logged-in user + return; + } - // Auth errors - if (!(error instanceof Error) && error.message) { - errorInfo = error; - message = error.message; - error = new Error(message); - // API errors - } else if (error.config && error.config.url) { - errorInfo.url = error.config.url; + let errorInfo = {} as ErrorInfoType + let message = String(error); + // typesafe version of our unknown error, always going to + // become an object for logging. + let err = {} + + if (error instanceof Error) { + // It is an error, we can go forth and report it. + err = error; + } else { + if (!(error instanceof Error) + && typeof error === 'object' + && error !== null) { + // At least it's an object, let's use it. + err = error; + // Let's cast it as an ErrorInfoType so we can check + // a couple more things. + errorInfo = error as ErrorInfoType; + + // If it has a message, assume auth error from Amplify Auth + if ('message' in errorInfo + && typeof errorInfo.message === 'string') { + message = errorInfo.message; + error = new Error(message); + } + + // Found Config, Assume API error from Amplify Axios + if ('config' in errorInfo + && typeof errorInfo.config === 'object' + && 'url' in errorInfo.config + ) { + errorInfo.url = errorInfo.config['url']; + } + } + + // If nothing else, make a new error using message from + // the start of all this. + if (typeof error !== 'object') { + err = new Error(message); + } } - logError(error, errorInfo); + logError(err, errorInfo); alert(message); } diff --git a/_chapters/review-our-app-architecture.md b/_chapters/review-our-app-architecture.md index 4603a225b..3751d4a58 100644 --- a/_chapters/review-our-app-architecture.md +++ b/_chapters/review-our-app-architecture.md @@ -20,7 +20,7 @@ API Gateway handles our main `/` endpoint, sending GET requests made to this to ### Notes App API Architecture -Then we added DynamoDB and S3 to the mix. We'll also be adding a few other Lambda functions. +Then we added DynamoDB and S3 to the mix. We will also be adding a few other Lambda functions. So our new notes app backend architecture will look something like this. @@ -31,8 +31,8 @@ There are a couple of things of note here: 1. Our database is not exposed publicly and is only invoked by our Lambda functions. 2. But our users will be uploading files directly to the S3 bucket that we created. -The second point is something that is different from a lot of traditional server based architectures. We are typically used to uploading the files to our server and then moving them to a file server. But here we'll be directly uploading it to our S3 bucket. We'll look at this in more detail when we look at file uploads. +The second point is something that is different from a lot of traditional server based architectures. We are typically used to uploading the files to our server and then moving them to a file server. But here we will be directly uploading it to our S3 bucket. We will look at this in more detail when we look at file uploads. -In the coming sections will also be looking at how we can secure access to these resources. We'll be setting it up such that only our authenticated users will be allowed to access these resources. +In the coming sections will also be looking at how we can secure access to these resources. We will be setting it up such that only our authenticated users will be allowed to access these resources. Now that we have a good idea of how our app will be architected, let's get to work! diff --git a/_chapters/save-changes-to-a-note.md b/_chapters/save-changes-to-a-note.md index 7846b072b..131f8487a 100644 --- a/_chapters/save-changes-to-a-note.md +++ b/_chapters/save-changes-to-a-note.md @@ -3,23 +3,23 @@ layout: post title: Save Changes to a Note date: 2017-01-30 00:00:00 lang: en -description: For a user to be able to edit a note in our React.js app, we need to make a PUT request to our severless backend API using AWS Amplify. We also need to allow them to upload files directly to S3 and add that as an attachment to the note. +description: For a user to be able to edit a note in our React.js app, we need to make a PUT request to our serverless backend API using AWS Amplify. We also need to allow them to upload files directly to S3 and add that as an attachment to the note. comments_id: save-changes-to-a-note/131 ref: save-changes-to-a-note --- Now that our note loads into our form, let's work on saving the changes we make to that note. -{%change%} Replace the `handleSubmit` function in `src/containers/Notes.js` with the following. +{%change%} Replace the `handleSubmit` function in `src/containers/Notes.tsx` with the following. -```js -function saveNote(note) { +```tsx +function saveNote(note: NotesType) { return API.put("notes", `/notes/${id}`, { body: note, }); } -async function handleSubmit(event) { +async function handleSubmit(event: React.FormEvent) { let attachment; event.preventDefault(); @@ -38,11 +38,13 @@ async function handleSubmit(event) { try { if (file.current) { attachment = await s3Upload(file.current); + } else if (note && note.attachment) { + attachment = note.attachment } await saveNote({ - content, - attachment: attachment || note.attachment, + content: content, + attachment: attachment, }); nav("/"); } catch (e) { @@ -54,7 +56,7 @@ async function handleSubmit(event) { {%change%} And include our `s3Upload` helper method in the header: -```js +```tsx import { s3Upload } from "../lib/awsLib"; ``` @@ -70,6 +72,6 @@ Let's switch over to our browser and give it a try by saving some changes. ![Notes page saving screenshot](/assets/notes-page-saving.png) -You might have noticed that we are not deleting the old attachment when we upload a new one. To keep things simple, we are leaving that bit of detail up to you. It should be pretty straightforward. Check the [AWS Amplify API Docs](https://aws.github.io/aws-amplify/api/classes/storageclass.html#remove) on how to a delete file from S3. +You might have noticed that we are not deleting the old attachment when we upload a new one. To keep things simple, we are leaving that bit of detail up to you. It should be pretty straightforward. Check the [AWS Amplify API Docs](https://aws.github.io/aws-amplify/api/classes/storageclass.html#remove){:target="_blank"} on how to a delete file from S3. Next up, let's allow users to delete their note. diff --git a/_chapters/secure-our-serverless-apis.md b/_chapters/secure-our-serverless-apis.md index f6716be19..f7a7f7817 100644 --- a/_chapters/secure-our-serverless-apis.md +++ b/_chapters/secure-our-serverless-apis.md @@ -17,77 +17,77 @@ Recall that we've been hard coding our user ids so far (with user id `123`). We' Recall the function signature of a Lambda function: -```js -export async function main(event, context) {} +```typescript +export async function main(event: APIGatewayEvent, context: Context) {} ``` Or the refactored version that we are using: -```js -export const main = handler(async (event) => {}); +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => {}); ``` So far we've used the `event` object to get the path parameters (`event.pathParameters`) and request body (`event.body`). Now we'll get the id of the authenticated user. -```js -event.requestContext.authorizer.iam.cognitoIdentity.identityId; +```typescript +event.requestContext.authorizer?.iam.cognitoIdentity.identityId; ``` This is an id that's assigned to our user by our Cognito Identity Pool. You'll also recall that so far all of our APIs are hard coded to interact with a single user. -```js +```typescript userId: "123", // The id of the author ``` Let's change that. -{%change%} Replace the above line in `packages/functions/src/create.js` with. +{%change%} Replace the above line in `packages/functions/src/create.ts` with. -```js -userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, +```typescript +userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, ``` -{%change%} Do the same in the `packages/functions/src/get.js`. +{%change%} Do the same in the `packages/functions/src/get.ts`. -```js -userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, +```typescript +userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, ``` -{%change%} And in the `packages/functions/src/update.js`. +{%change%} And in the `packages/functions/src/update.ts`. -```js -userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, +```typescript +userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, ``` -{%change%} In `packages/functions/src/delete.js` as well. +{%change%} In `packages/functions/src/delete.ts` as well. -```js -userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, +```typescript +userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, ``` -{%change%} In `packages/functions/src/list.js` find this line instead. +{%change%} In `packages/functions/src/list.ts` find this line instead. -```js +```typescript ":userId": "123", ``` {%change%} And replace it with. -```js -":userId": event.requestContext.authorizer.iam.cognitoIdentity.identityId, +```typescript +":userId": event.requestContext.authorizer?.iam.cognitoIdentity.identityId, ``` {%change%} Also, include `event` in the function arguments. -```js -export const main = handler(async (event) => { +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => { ``` -Keep in mind that the `userId` above is the Federated Identity id (or Identity Pool user id). This is not the user id that is assigned in our User Pool. If you want to use the user's User Pool user Id instead, have a look at the [Mapping Cognito Identity Id and User Pool Id]({% link _chapters/mapping-cognito-identity-id-and-user-pool-id.md %}) chapter. +Keep in mind that the `userId` above is the Federated Identity id (or Identity Pool user id). This is not the user id that is assigned in our User Pool. If you want to use the user's User Pool user Id instead, have a look at the [Mapping Cognito Identity Id and User Pool Id]({% link _chapters/mapping-cognito-identity-id-and-user-pool-id.md %}){:target="_blank"} chapter. To test these changes we cannot use the `curl` command anymore. We'll need to generate a set of authentication headers to make our requests. Let's do that next. @@ -99,22 +99,23 @@ To be able to hit our API endpoints securely, we need to follow these steps. 1. Authenticate against our User Pool and acquire a user token. 2. With the user token get temporary IAM credentials from our Identity Pool. -3. Use the IAM credentials to sign our API request with [Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html). +3. Use the IAM credentials to sign our API request with [Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html){:target="_blank"}. -These steps can be a bit tricky to do by hand. So we created a simple tool called [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test). +These steps can be a bit tricky to do by hand. So we created a simple tool called [AWS API Gateway Test CLI](https://github.com/AnomalyInnovations/aws-api-gateway-cli-test){:target="_blank"}. -You can run it using. +You can also run it using `pnpm dlx` as seen in the following example. (If you have installed the tool you can use `apig-test` in place of `pnpm dlx` ). ```bash -$ npx aws-api-gateway-cli-test \ +$ pnpm dlx aws-api-gateway-cli-test \ +--user-pool-id='' \ +--app-client-id='' \ +--cognito-region='' \ +--identity-pool-id='' \ +--invoke-url='' \ +--api-gateway-region='' \ +\ --username='admin@example.com' \ --password='Passw0rd!' \ ---user-pool-id='USER_POOL_ID' \ ---app-client-id='USER_POOL_CLIENT_ID' \ ---cognito-region='COGNITO_REGION' \ ---identity-pool-id='IDENTITY_POOL_ID' \ ---invoke-url='API_ENDPOINT' \ ---api-gateway-region='API_REGION' \ --path-template='/notes' \ --method='POST' \ --body='{"content":"hello world","attachment":"hello.jpg"}' @@ -125,15 +126,17 @@ We need to pass in quite a bit of our info to complete the above steps. - Use the username and password of the user created above. - Replace `USER_POOL_ID`, `USER_POOL_CLIENT_ID`, `COGNITO_REGION`, and `IDENTITY_POOL_ID` with the `UserPoolId`, `UserPoolClientId`, `Region`, and `IdentityPoolId` from our [previous chapter]({% link _chapters/adding-auth-to-our-serverless-app.md %}). - Replace the `API_ENDPOINT` with the `ApiEndpoint` from our [API stack outputs]({% link _chapters/add-an-api-to-create-a-note.md %}). -- And for the `API_REGION` you can use the same `Region` as we used above. Since our entire app is deployed to the same region. +- And for the `API_REGION` you can use the same `Region` as we used above. Since our entire app is deployed to the same region. (See Console output from auth for key **`apigTestFlags`**. You can replace the part above the extra slash with those values. ) While this might look intimidating, just keep in mind that behind the scenes all we are doing is generating some security headers before making a basic HTTP request. We won't need to do this when we connect from our React.js app. -If you are on Windows, use the command below. The space between each option is very important. +{%aside%} +If you are on Windows, you can use the command below. The spaces between each option are very important. ```bash $ npx aws-api-gateway-cli-test --username admin@example.com --password Passw0rd! --user-pool-id USER_POOL_ID --app-client-id USER_POOL_CLIENT_ID --cognito-region COGNITO_REGION --identity-pool-id IDENTITY_POOL_ID --invoke-url API_ENDPOINT --api-gateway-region API_REGION --path-template /notes --method POST --body '{""content\":\"hello world\",\"attachment\":\"hello.jpg\"}' ``` +{%endaside%} If the command is successful, the response will look similar to this. @@ -154,7 +157,7 @@ Making API request } ``` -It'll have created a new note for our test user in the **DynamoDB** tab of the [SST Console]({{ site.old_console_url }}). +It'll have created a new note for our test user in the **DynamoDB** tab of the [SST Console]({{ site.old_console_url }}){:target="_blank"}. ![SST Console test user new note](/assets/part2/sst-console-test-user-new-note.png) diff --git a/_chapters/setting-serverless-environments-variables-in-a-react-app.md b/_chapters/setting-serverless-environments-variables-in-a-react-app.md index 443092b87..11a72ddfb 100644 --- a/_chapters/setting-serverless-environments-variables-in-a-react-app.md +++ b/_chapters/setting-serverless-environments-variables-in-a-react-app.md @@ -33,9 +33,9 @@ Here's what we want to happening when developing locally: 3. Then start our local React development environment. 4. It should automatically pick up the backend environment variables. -As an example, let's look at a really simple full-stack [SST app](/). It has a simple _Hello World_ API endpoint. And a React.js app. +As an example, let's look at a really simple full-stack [SST app](/){:target="_blank"}. It has a simple _Hello World_ API endpoint. And a React.js app. -```js +```typescript import * as sst from "@serverless-stack/resources"; export default class MyStack extends sst.Stack { @@ -69,7 +69,7 @@ export default class MyStack extends sst.Stack { Here we are using the [`ReactStaticSite`]({{ site.docs_url }}/constructs/ReactStaticSite) construct. It allows us to set React environment variables from our API. -```js +```typescript environment: { // Pass in the API endpoint to our app REACT_APP_API_URL: api.url, @@ -79,7 +79,7 @@ environment: { Now when we start our local development environment. ```bash -$ npm start +$ pnpm start ``` SST generates a file in the `.build/` directory with the environment that we configured. It looks something like this. @@ -96,10 +96,10 @@ SST generates a file in the `.build/` directory with the environment that we con ] ``` -On the React side, we'll now want to pick the environment variable up. To do this, we'll use a really simple CLI ([`@serverless-stack/static-site-env`](https://www.npmjs.com/package/@serverless-stack/static-site-env)) that reads from this file and sets it as a [build-time environment variable in React](https://create-react-app.dev/docs/adding-custom-environment-variables/). +On the React side, we'll now want to pick the environment variable up. To do this, we'll use a really simple CLI ([`@serverless-stack/static-site-env`](https://www.npmjs.com/package/@serverless-stack/static-site-env){:target="_blank"}) that reads from this file and sets it as a [build-time environment variable in React](https://create-react-app.dev/docs/adding-custom-environment-variables/){:target="_blank"}. ```bash -$ npm install @serverless-stack/static-site-env --save-dev +$ pnpm add --save-dev @serverless-stack/static-site-env ``` We can use the environment variable in our components using `process.env.REACT_APP_API_URL`. @@ -130,7 +130,7 @@ We can now wrap our start script with it. So if we start our React local environment: ```bash -$ npm run start +$ pnpm run start ``` It'll contain the environment variable that we had previously set in our serverless app! @@ -141,17 +141,17 @@ Next, let's look at what happens when we deploy our full-stack app. ## While Deploying -We need our React app to be deployed with our environment variables. SST uses [CDK]({% link _chapters/what-is-aws-cdk.md %}) internally, so the flow looks something like this. +We need our React app to be deployed with our environment variables. SST uses [CDK]({% link _chapters/what-is-aws-cdk.md %}){:target="_blank"} internally, so the flow looks something like this. 1. Deploy our API. 2. Build our React app. 3. Replace the environment variables in our React app. 4. Deploy our React app to S3 and CloudFront. -[SST](/) and the [`ReactStaticSite`]({{ site.docs_url }}/constructs/ReactStaticSite) construct do this automatically for you. +[SST](/){:target="_blank"} and the [`ReactStaticSite`]({{ site.docs_url }}/constructs/ReactStaticSite){:target="_blank"} construct do this automatically for you. ![Serverless environment variable set in a React app deployed to AWS](/assets/extra-credit/serverless-environment-variable-set-in-a-react-app-deployed-to-aws.png) And that's it! You now have a full-stack serverless app where the environment variables from your backend are automatically set in your React app. You don't need to hard code them anymore and they work in your local development environment as well! -For further details, check out our example on building a React.js app with SST: [**How to create a React.js app with serverless**]({% link _examples/how-to-create-a-reactjs-app-with-serverless.md %}) +For further details, check out our example on building a React.js app with SST: [**How to create a React.js app with serverless**]({% link _examples/how-to-create-a-reactjs-app-with-serverless.md %}){:target="_blank"}. diff --git a/_chapters/setting-up-your-project-on-seed.md b/_chapters/setting-up-your-project-on-seed.md index 5ba91c63c..3c34b0d7b 100644 --- a/_chapters/setting-up-your-project-on-seed.md +++ b/_chapters/setting-up-your-project-on-seed.md @@ -24,7 +24,7 @@ Now to add your project, select **GitHub** as your git provider. You'll be asked Select the repo we've been using so far. -Next, Seed will automatically detect the `sst.json` config in your repo. Click **Add Service**. +Next, Seed will automatically detect the `sst.config.ts` file in your repo. Click **Add Service**. ![SST app detected](/assets/part2/sst-app-detected.png) @@ -50,9 +50,9 @@ Fill in the credentials and click **Add a New App**. ![Add AWS IAM credentials](/assets/part2/add-aws-iam-credentials.png) -Your new app is created. You'll notice a few things here. First, we have a service called **notes**. It's picking up the name from our `sst.json`. You can choose to change this by clicking on the service and editing its name. You'll also notice the two stages that have been created. +Your new app is created. You'll notice a few things here. First, we have a service called **notes**. It's picking up the name from our `sst.config.ts` file. You can choose to change this by clicking on the service and editing its name. You'll also notice the two stages that have been created. -Our app can have multiple services within it. A service (roughly speaking) is a reference to a `sst.json` or `serverless.yml` file (for Serverless Framework). In our case we just have the one service. +Our app can have multiple services within it. A service (roughly speaking) is a reference to a `sst.config.ts` or `serverless.yml` file (for Serverless Framework). In our case we just have the one service. ![Seed app homepage](/assets/part2/seed-app-homepage.png) diff --git a/_chapters/setup-a-stripe-account.md b/_chapters/setup-a-stripe-account.md index 8d1bf34f9..253fba5f2 100644 --- a/_chapters/setup-a-stripe-account.md +++ b/_chapters/setup-a-stripe-account.md @@ -29,22 +29,14 @@ Let's start by creating a free Stripe account. Head over to [Stripe](https://das ![Create a Stripe account screenshot](/assets/part2/create-a-stripe-account.png) -Once signed in, click the **Developers** link on the left. +Once signed in with a confirmed account, you will be able to use the developer tools. -![Stripe dashboard screenshot](/assets/part2/stripe-dashboard.png) +![Stripe dashboard screenshot](/assets/stripe/dashboard.png) -And hit **API keys**. +The first thing to do is switch to test mode. This is important because we don't want to charge our credit card every time we test our app. -![Developer section in Stripe dashboard screenshot](/assets/part2/developer-section-in-stripe-dashboard.png) +The second thing to note is that Stripe has automatically generated a test and live **Publishable key** and a test and live **Secret key**. The Publishable key is what we are going to use in our frontend client with the Stripe SDK. And the Secret key is what we are going to use in our API when asking Stripe to charge our user. As denoted, the Publishable key is public while the Secret key needs to stay private. -The first thing to note here is that we are working with a test version of API keys. To create the live version, you'd need to verify your email address and business details to activate your account. For the purpose of this guide we'll continue working with our test version. - -The second thing to note is that we need to generate the **Publishable key** and the **Secret key**. The Publishable key is what we are going to use in our frontend client with the Stripe SDK. And the Secret key is what we are going to use in our API when asking Stripe to charge our user. As denoted, the Publishable key is public while the Secret key needs to stay private. - -Hit the **Reveal test key token**. - -![Stripe dashboard Stripe API keys screenshot](/assets/part2/stripe-dashboard-stripe-api-keys.png) - -Make a note of both the **Publishable key** and the **Secret key**. We are going to be using these later. +Make a note of both the **Publishable test key** and the **Secret test key**. We are going to be using these later. Next, let's use this in our SST app. diff --git a/_chapters/setup-an-error-boundary-in-react.md b/_chapters/setup-an-error-boundary-in-react.md index c3692048a..7fb4dab3c 100644 --- a/_chapters/setup-an-error-boundary-in-react.md +++ b/_chapters/setup-an-error-boundary-in-react.md @@ -16,34 +16,35 @@ An Error Boundary is a component that allows us to catch any errors that might h It's incredibly straightforward to setup. So let's get started. -{%change%} Add the following to `src/components/ErrorBoundary.js` in your `frontend/` directory. +{%change%} Add the following to `src/components/ErrorBoundary.tsx` in your `frontend/` directory. -```jsx +```tsx import React from "react"; -import { logError } from "../lib/errorLib"; +import {logError} from "../lib/errorLib"; import "./ErrorBoundary.css"; -export default class ErrorBoundary extends React.Component { +export default class ErrorBoundary extends React.Component { state = { hasError: false }; - static getDerivedStateFromError(error) { + static getDerivedStateFromError(_error: unknown) { return { hasError: true }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: any) { logError(error, errorInfo); } render() { - return this.state.hasError ? ( -
+ if (this.state.hasError) { + return

Sorry there was a problem loading this page

-
- ) : ( - this.props.children - ); +
; + } else { + return this.props.children; + } } } + ``` The key part of this component is the `componentDidCatch` and `getDerivedStateFromError` methods. These get triggered when any of the child components have an unhandled error. We set the internal state, `hasError` to `true` to display our fallback UI. And we report the error to Sentry by calling `logError` with the `error` and `errorInfo` that comes with it. @@ -64,25 +65,25 @@ The styles we are using are very similar to our `NotFound` component. We use tha To use the Error Boundary component that we created, we'll need to add it to our app component. -{%change%} Find the following in `src/App.js`. +{%change%} Find the following in `src/App.tsx`. {% raw %} -```jsx - +```tsx + ``` {% endraw %} -{%change%} And replace it with: +{%change%} And wrap it with our `ErrorBoundary`: {% raw %} -```jsx +```tsx - + @@ -92,7 +93,7 @@ To use the Error Boundary component that we created, we'll need to add it to our {%change%} Also, make sure to import it in the header of `src/App.js`. -```js +```tsx import ErrorBoundary from "./components/ErrorBoundary"; ``` @@ -100,45 +101,40 @@ And that's it! Now an unhandled error in our containers will show a nice error m ### Commit the Changes -{%change%} Let's quickly commit these to Git. +{%change%} Let's commit these to Git (but don't push yet). ```bash -$ git add . -$ git commit -m "Adding React error reporting" +$ git add .;git commit -m "Adding React error reporting"; ``` ### Test the Error Boundary Before we move on, let's do a quick test. -Replace the following in `src/containers/Home.js`. +Replace the following in `src/containers/Home.tsx`. -```js -{ - isAuthenticated ? renderNotes() : renderLander(); -} +```tsx +{isAuthenticated ? renderNotes() : renderLander()} ``` With these faulty lines: {% raw %} -```js -{ - isAuthenticated ? renderNotes() : renderLander(); -} -{ - isAuthenticated.none.no; -} +```tsx +{isAuthenticated ? renderNotes() : renderLander()} +{ isAuthenticated.none.no } ``` - + {% endraw %} Now in your browser you should see something like this. ![React error message](/assets/monitor-debug-errors/react-error-message.png) -Note that, you'll need to have the SST local development environment (`npm start`) and React local environment (`npm run start`) running. +{%note%} +You'll need to have the SST local development environment (`npm start`) and React local environment (`npm run start`) running. +{%endnote%} While developing, React doesn't show your Error Boundary fallback UI by default. To view that, hit the **close** button on the top right. diff --git a/_chapters/setup-bootstrap.md b/_chapters/setup-bootstrap.md index 9fd5b0248..1d924176e 100644 --- a/_chapters/setup-bootstrap.md +++ b/_chapters/setup-bootstrap.md @@ -8,25 +8,25 @@ description: Bootstrap is a UI framework that makes it easy to build consistent comments_id: set-up-bootstrap/118 --- -A big part of writing web applications is having a UI Kit to help create the interface of the application. We are going to use [Bootstrap](http://getbootstrap.com) for our note taking app. While Bootstrap can be used directly with React; the preferred way is to use it with the [React Bootstrap](https://react-bootstrap.github.io) package. This makes our markup a lot simpler to implement and understand. +A big part of writing web applications is having a UI Kit to help create the interface of the application. We are going to use [Bootstrap](http://getbootstrap.com){:target="_blank"} for our note taking app. While Bootstrap can be used directly with React; the preferred way is to use it with the [React Bootstrap](https://react-bootstrap.github.io){:target="_blank"} package. This makes our markup a lot simpler to implement and understand. -We also need a couple of icons in our application. We'll be using the [React Icons](https://react-icons.github.io/react-icons/) package for this. It allows us to include icons in our React app as standard React components. +We also need a couple of icons in our application. We'll be using the [React Icons](https://react-icons.github.io/react-icons/){:target="_blank"} package for this. It allows us to include icons in our React app as standard React components. ### Installing React Bootstrap {%change%} Run the following command in your `frontend/` directory and **not** in your project root ```bash -$ npm install bootstrap react-bootstrap react-icons +$ pnpm add --save bootstrap react-bootstrap react-icons;pnpm add --save-dev @types/bootstrap @types/react-bootstrap ``` -This installs the npm packages and adds the dependencies to your `package.json` of your React app. +This installs the packages and dependencies to the `package.json` of your React app. ### Add Bootstrap Styles -{%change%} React Bootstrap uses the standard Bootstrap v5 styles; so just add the following styles to your `src/index.js`. +{%change%} React Bootstrap uses the standard Bootstrap v5 styles; so just add the following styles to your `src/index.tsx`. -```js +```typescript import "bootstrap/dist/css/bootstrap.min.css"; ``` @@ -47,6 +47,6 @@ input[type="file"] { We are also setting the width of the input type file to prevent the page on mobile from overflowing and adding a scrollbar. -Now if you head over to your browser, you might notice that the styles have shifted a bit. This is because Bootstrap includes [Normalize.css](http://necolas.github.io/normalize.css/) to have a more consistent styles across browsers. +Now if you head over to your browser, you might notice that the styles have shifted a bit. This is because Bootstrap includes [Normalize.css](http://necolas.github.io/normalize.css/){:target="_blank"} to have a more consistent styles across browsers. Next, we are going to create a few routes for our application and set up the React Router. diff --git a/_chapters/setup-custom-fonts.md b/_chapters/setup-custom-fonts.md index bb6b3b20c..a7ad7e4a8 100644 --- a/_chapters/setup-custom-fonts.md +++ b/_chapters/setup-custom-fonts.md @@ -8,17 +8,17 @@ description: To use custom fonts in our React.js project we are going to use Goo comments_id: set-up-custom-fonts/81 --- -Custom Fonts are now an almost standard part of modern web applications. We'll be setting it up for our note taking app using [Google Fonts](https://fonts.google.com). +Custom Fonts are now an almost standard part of modern web applications. We'll be setting it up for our note taking app using [Google Fonts](https://fonts.google.com){:target="_blank"}. This also gives us a chance to explore the structure of our newly created React.js app. ### Include Google Fonts -For our project we'll be using the combination of a Serif ([PT Serif](https://fonts.google.com/specimen/PT+Serif)) and Sans-Serif ([Open Sans](https://fonts.google.com/specimen/Open+Sans)) typeface. They will be served out through Google Fonts and can be used directly without having to host them on our end. +For our project we'll be using the combination of a Serif ([PT Serif](https://fonts.google.com/specimen/PT+Serif){:target="_blank"}) and Sans-Serif ([Open Sans](https://fonts.google.com/specimen/Open+Sans){:target="_blank"}) typeface. They will be served out through Google Fonts and can be used directly without having to host them on our end. Let's first include them in the HTML. Our React.js app is using a single HTML file. -{%change%} Go ahead and edit `public/index.html` and add the following line in the `` section of the HTML to include the two typefaces. +{%change%} Edit `public/index.html` and add the following line in the `` section of the HTML to include the two typefaces. ``` html ; // Log AWS SDK calls AWS.config.logger = { log: debug }; -export default function debug() { +export default function debug(...args: Array) { logs.push({ date: new Date(), - string: util.format.apply(null, arguments), + string: util.format.apply(null, [...args]), }); } -export function init(event) { +export function init(event: APIGatewayEvent) { logs = []; // Log API event @@ -52,9 +57,9 @@ export function init(event) { }); } -export function flush(e) { +export function flush(error: unknown) { logs.forEach(({ date, string }) => console.debug(date, string)); - console.error(e); + console.error(error); } ``` @@ -89,7 +94,7 @@ We are doing a few things of note in this simple helper. So in our Lambda function code, if we want to log some debug information that only gets printed out if we have an error, we'll do the following: -```js +```typescript debug( "This stores the message and prints to CloudWatch if Lambda function later throws an exception" ); @@ -97,7 +102,7 @@ debug( In contrast, if we always want to log to CloudWatch, we'll: -```js +```typescript console.log("This prints a message in CloudWatch prefixed with INFO"); console.warn("This prints a message in CloudWatch prefixed with WARN"); console.error("This prints a message in CloudWatch prefixed with ERROR"); @@ -111,13 +116,14 @@ You'll recall that all our Lambda functions are wrapped using a `handler()` meth We'll use the debug lib that we added above to improve our error handling. -{%change%} Replace our `packages/core/src/handler.js` with the following. +{%change%} Replace our `packages/core/src/handler.ts` with the following. -```js +```typescript +import { Context, APIGatewayEvent } from 'aws-lambda'; import * as debug from "./debug"; -export default function handler(lambda) { - return async function (event, context) { +export default function handler(lambda: Function) { + return async function (event: APIGatewayEvent, context: Context) { let body, statusCode; // Start debugger @@ -127,11 +133,15 @@ export default function handler(lambda) { // Run the Lambda body = await lambda(event, context); statusCode = 200; - } catch (e) { + } catch (error) { // Print debug messages - debug.flush(e); + debug.flush(error); - body = { error: e.message }; + if (error instanceof Error) { + body = {error: error.message}; + } else { + body = {error: String(error)}; + } statusCode = 500; } @@ -160,7 +170,7 @@ This should be fairly straightforward: You might recall the way we are currently using the above error handler in our Lambda functions. -```js +```typescript export const main = handler((event, context) => { // Do some work const a = 1 + 1; @@ -171,7 +181,9 @@ export const main = handler((event, context) => { We wrap all of our Lambda functions using the error handler. -Note that, the `handler.js` needs to be **imported before we import anything else**. This is because the `debug.js` that it imports needs to initialize AWS SDK logging before it's used anywhere else. +Note that, the `handler.ts` needs to be **imported before we import anything else**. This is because the `debug.js` that it imports needs to initialize AWS SDK logging before it's used anywhere else. + +{%change%} Go check and make sure you have imported handler first. ### Commit the Code diff --git a/_chapters/setup-error-reporting-in-react.md b/_chapters/setup-error-reporting-in-react.md index 259e2d885..11c147a97 100644 --- a/_chapters/setup-error-reporting-in-react.md +++ b/_chapters/setup-error-reporting-in-react.md @@ -32,7 +32,7 @@ For the type of project, select **React**. ![Sentry select React project](/assets/monitor-debug-errors/sentry-select-react-project.png) -Give your project a name. +Give your project a name. ![Sentry name React project](/assets/monitor-debug-errors/sentry-name-react-project.png) @@ -45,17 +45,21 @@ And that's it. Scroll down and copy the `Sentry.init` line. {%change%} Now head over to the React `frontend/` directory and install Sentry. ```bash -$ npm install @sentry/browser --save +$ pnpm add @sentry/react --save ``` We are going to be using Sentry across our app. So it makes sense to keep all the Sentry related code in one place. -{%change%} Add the following to the top of your `src/lib/errorLib.js`. +{%change%} Add the following to the top of your `src/lib/errorLib.ts`. -```js -import * as Sentry from "@sentry/browser"; +```typescript +import * as Sentry from "@sentry/react"; import config from "../config"; +export interface ErrorInfoType { + [key: string | symbol]: string; +} + const isLocal = process.env.NODE_ENV === "development"; export function initSentry() { @@ -66,7 +70,7 @@ export function initSentry() { Sentry.init({ dsn: config.SENTRY_DSN }); } -export function logError(error, errorInfo = null) { +export function logError(error: unknown, errorInfo: ErrorInfoType | null = null) { if (isLocal) { return; } @@ -78,9 +82,9 @@ export function logError(error, errorInfo = null) { } ``` -{%change%} Add the `SENTRY_DSN` below the `const config = {` line in `src/config.js`. +{%change%} Add the `SENTRY_DSN` below the `const config = {` line in `frontend/src/config.ts`. -```js +```typescript SENTRY_DSN: "https://your-dsn-id-here@sentry.io/123456", ``` @@ -95,12 +99,13 @@ The `logError` method is what we are going to call when we want to report an err Next, let's initialize our app with Sentry. -{%change%} Add the following to the end of the imports in `src/index.js`. +{%change%} Add the following to the end of the imports in `src/index.tsx`. -```js +```typescript import { initSentry } from "./lib/errorLib"; -initSentry(); +initSentry() + ``` Now we are ready to start reporting errors in our React app! Let's start with the API errors. diff --git a/_chapters/signup-with-aws-cognito.md b/_chapters/signup-with-aws-cognito.md index d4c256597..e80a092e4 100644 --- a/_chapters/signup-with-aws-cognito.md +++ b/_chapters/signup-with-aws-cognito.md @@ -10,10 +10,10 @@ comments_id: signup-with-aws-cognito/130 Now let's go ahead and implement the `handleSubmit` and `handleConfirmationSubmit` functions and connect it up with our AWS Cognito setup. -{%change%} Replace our `handleSubmit` and `handleConfirmationSubmit` functions in `src/containers/Signup.js` with the following. +{%change%} Replace our `handleSubmit` and `handleConfirmationSubmit` functions in `src/containers/Signup.tsx` with the following. -```js -async function handleSubmit(event) { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); setIsLoading(true); try { @@ -29,7 +29,7 @@ async function handleSubmit(event) { } } -async function handleConfirmationSubmit(event) { +async function handleConfirmationSubmit(event: React.FormEvent) { event.preventDefault(); setIsLoading(true); try { @@ -44,10 +44,18 @@ async function handleConfirmationSubmit(event) { } ``` -{%change%} Also, include the Amplify Auth in our header. +{%change%} Also, include the Amplify Auth, onError, and ISignUpResult Type in our header. -```js +```tsx import { Auth } from "aws-amplify"; +import {onError} from "../lib/errorLib"; +import {ISignUpResult} from "amazon-cognito-identity-js"; +``` + +{%change%} Finally, replace the constant for newUser and setNewUser with the following: + +```tsx +const [newUser, setNewUser] = useState(null); ``` The flow here is pretty simple: @@ -74,7 +82,7 @@ A quick note on the signup flow here. If the user refreshes their page at the co 1. Check for the `UsernameExistsException` in the `handleSubmit` function's `catch` block. -2. Use the `Auth.resendSignUp()` method to resend the code if the user has not been previously confirmed. Here is a link to the [Amplify API docs](https://aws.github.io/aws-amplify/api/classes/authclass.html#resendsignup). +2. Use the `Auth.resendSignUp()` method to resend the code if the user has not been previously confirmed. Here is a link to the [Amplify API docs](https://aws.github.io/aws-amplify/api/classes/authclass.html#resendsignup){:target="_blank"}. 3. Confirm the code just as we did before. @@ -84,13 +92,15 @@ Now while developing you might run into cases where you need to manually confirm ```bash aws cognito-idp admin-confirm-sign-up \ - --region COGNITO_REGION \ - --user-pool-id USER_POOL_ID \ - --username YOUR_USER_EMAIL + --region \ + --user-pool-id \ + --username ``` -Just be sure to use your Cognito User Pool Id and the email you used to create the account. +Just be sure to use your Cognito `USER_POOL_ID` and the _email address_ you used to create the account. -If you would like to allow your users to change their email or password, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}). +{%aside%} +If you would like to allow your users to change their email or password, you can refer to our [Extra Credit series of chapters on user management]({% link _chapters/manage-user-accounts-in-aws-amplify.md %}){:target="_blank"}. +{%endaside%} Next up, we are going to create our first note. diff --git a/_chapters/unexpected-errors-in-lambda-functions.md b/_chapters/unexpected-errors-in-lambda-functions.md index f8b551f2a..56b8a10e8 100644 --- a/_chapters/unexpected-errors-in-lambda-functions.md +++ b/_chapters/unexpected-errors-in-lambda-functions.md @@ -14,18 +14,25 @@ Previously, we looked at [how to debug errors in our Lambda function code]({% li Our Lambda functions often make API requests to interact with other services. In our notes app, we talk to DynamoDB to store and fetch data; and we also talk to Stripe to process payments. When we make an API request, there is the chance the HTTP connection times out or the remote service takes too long to respond. We are going to look at how to detect and debug the issue. The default timeout for Lambda functions are 6 seconds. So let's simulate a timeout using `setTimeout`. -{%change%} Replace the `main` function in `packages/functions/src/get.js` with the following. +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. + +```typescript +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } -```js -export const main = handler(async (event) => { const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved - // - 'userId': Identity Pool identity id of the authenticated user - // - 'noteId': path parameter + // 'Key' defines the partition key and sort key of + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - noteId: event.pathParameters.id, + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path }, }; @@ -72,23 +79,30 @@ Next let's look at what happens when our Lambda function runs out of memory. By default, a Lambda function has 1024MB of memory. You can assign any amount of memory between 128MB and 3008MB in 64MB increments. So in our code, let's try and allocate more memory till it runs out of memory. -{%change%} Replace the `main` function in `packages/functions/src/get.js` with the following. +{%change%} Replace the `main` function in `packages/functions/src/get.ts` with the following. -```js -function allocMem() { - let bigList = Array(4096000).fill(1); +```typescript +function allocMem():Array { + let bigList: Array = Array(4096000).fill(1); return bigList.concat(allocMem()); } -export const main = handler(async (event) => { +export const main = handler(async (event: APIGatewayProxyEvent) => { + let path_id + + if (!event.pathParameters || !event.pathParameters.id || event.pathParameters.id.length == 0) { + throw new Error("Please provide the 'id' parameter."); + } else { + path_id = event.pathParameters.id + } + const params = { TableName: Table.Notes.tableName, - // 'Key' defines the partition key and sort key of the item to be retrieved - // - 'userId': Identity Pool identity id of the authenticated user - // - 'noteId': path parameter + // 'Key' defines the partition key and sort key of + // the item to be retrieved Key: { - userId: event.requestContext.authorizer.iam.cognitoIdentity.identityId, - noteId: event.pathParameters.id, + userId: event.requestContext.authorizer?.iam.cognitoIdentity.identityId, + noteId: path_id, // The id of the note from the path }, }; @@ -106,9 +120,9 @@ export const main = handler(async (event) => { Now we'll set our Lambda function to use the lowest memory allowed. -{%change%} Add the following below the `defaults: {` line in your `stacks/ApiStack.js`. +{%change%} Add the following below the `defaults: {` line in your `stacks/ApiStack.ts`. -```js +```typescript memorySize: 128, ``` @@ -124,7 +138,7 @@ Head over to your Seed dashboard and deploy it. Then, in your notes app, try and Just as before, you'll see the error in Sentry. And head over to new issue in Seed. -![Memory error details in Seed](/assets/monitor-debug-errors/memory-error-details-in-seed.png) + !`[Memory error details in Seed]`(/assets/monitor-debug-errors/memory-error-details-in-seed.png) Note the request took all of 128MB of memory. Click to expand the request. diff --git a/_chapters/unit-tests-in-serverless.md b/_chapters/unit-tests-in-serverless.md index 7c649a9d5..da583798b 100644 --- a/_chapters/unit-tests-in-serverless.md +++ b/_chapters/unit-tests-in-serverless.md @@ -10,19 +10,25 @@ comments_id: unit-tests-in-serverless/173 Our serverless app is made up of two big parts; the code that defines our infrastructure and the code that powers our Lambda functions. We'd like to be able to test both of these. -On the infrastructure side, we want to make sure the right type of resources are being created. So we don't mistakingly deploy some updates. +On the infrastructure side, we want to make sure the right type of resources are being created. So we don't mistakenly deploy some updates. On the Lambda function side, we have some simple business logic that figures out exactly how much to charge our user based on the number of notes they want to store. We want to make sure that we test all the possible cases for this before we start charging people. -SST comes with built in support for writing and running tests. It uses [Vitest](https://vitest.dev) internally for this. +SST comes with built in support for writing and running tests. It uses [Vitest](https://vitest.dev){:target="_blank"} internally for this. ### Testing CDK Infrastructure Let's start by writing a test for the CDK infrastructure in our app. We are going to keep this fairly simple for now. -{%change%} Add the following to `stacks/test/StorageStack.test.js`. +{%change%} Add vite for the workspace. -```js +```bash +$ pnpm add --save-dev --workspace-root vitest +``` + +{%change%} Add the following to `stacks/test/StorageStack.test.ts`. + +```typescript import { Template } from "aws-cdk-lib/assertions"; import { initProject } from "sst/project"; import { App, getStack } from "sst/constructs"; @@ -42,15 +48,15 @@ it("Test StorageStack", async () => { }); ``` -This is a very simple CDK test that checks if our storage stack creates a DynamoDB table and that the table's billing mode is set to `PAY_PER_REQUEST`. This is the default setting in SST's [`Table`]({{ site.docs_url }}/constructs/Table) construct. This test is making sure that we don't change this setting by mistake. +This is a very simple CDK test that checks if our storage stack creates a DynamoDB table and that the table's billing mode is set to `PAY_PER_REQUEST`. This is the default setting in SST's [`Table`]({{ site.docs_url }}/constructs/Table){:target="_blank"} construct. This test is making sure that we don't change this setting by mistake. ### Testing Lambda Functions We are also going to test the business logic in our Lambda functions. -{%change%} Create a new file in `packages/core/test/cost.test.js` and add the following. +{%change%} Create a new file in `packages/core/test/cost.test.ts` and add the following. -```js +```typescript import { expect, test } from "vitest"; import { calculateCost } from "../src/cost"; @@ -88,16 +94,16 @@ This should be straightforward. We are adding 3 tests. They are testing the diff Now let's add a test script. -{%change%} Add the following to the `scripts` in your `packages.json`. +{%change%} Add the following to the `scripts` in your `package.json`. -```js +```typescript "test": "sst bind vitest run", ``` -And we can run our tests by using the following command in the root of our project. +And we can run our tests by using the following command **in the root** of our project. ```bash -$ npm test +$ pnpm test ``` You should see something like this: diff --git a/_chapters/upload-a-file-to-s3.md b/_chapters/upload-a-file-to-s3.md index 9a4803b30..d8e4ea9ad 100644 --- a/_chapters/upload-a-file-to-s3.md +++ b/_chapters/upload-a-file-to-s3.md @@ -14,18 +14,18 @@ Let's now add an attachment to our note. The flow we are using here is very simp 2. The file is uploaded to S3 under the user's folder and we get a key back. 3. Create a note with the file key as the attachment. -We are going to use the Storage module that AWS Amplify has. If you recall, that back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}) chapter we allow a logged in user access to a folder inside our S3 Bucket. AWS Amplify stores directly to this folder if we want to _privately_ store a file. +We are going to use the Storage module that AWS Amplify has. If you recall, that back in the [Create a Cognito identity pool]({% link _chapters/create-a-cognito-identity-pool.md %}){:target="_blank"} chapter we allow a logged in user access to a folder inside our S3 Bucket. AWS Amplify stores directly to this folder if we want to _privately_ store a file. Also, just looking ahead a bit; we will be uploading files when a note is created and when a note is edited. So let's create a simple convenience method to help with that. ### Upload to S3 -{%change%} Create `src/lib/awsLib.js` and add the following: +{%change%} Create `src/lib/awsLib.ts` and add the following: -```js +```typescript import { Storage } from "aws-amplify"; -export async function s3Upload(file) { +export async function s3Upload(file: File) { const filename = `${Date.now()}-${file.name}`; const stored = await Storage.vault.put(filename, file, { @@ -50,10 +50,10 @@ The above method does a couple of things. Now that we have our upload methods ready, let's call them from the create note method. -{%change%} Replace the `handleSubmit` method in `src/containers/NewNote.js` with the following. +{%change%} Replace the `handleSubmit` method in `src/containers/NewNote.tsx` with the following. -```js -async function handleSubmit(event) { +```tsx +async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) { @@ -79,9 +79,9 @@ async function handleSubmit(event) { } ``` -{%change%} And make sure to include `s3Upload` by adding the following to the header of `src/containers/NewNote.js`. +{%change%} And make sure to include `s3Upload` by adding the following to the header of `src/containers/NewNote.tsx`. -```js +```tsx import { s3Upload } from "../lib/awsLib"; ``` @@ -94,3 +94,19 @@ The change we've made in the `handleSubmit` is that: Now when we switch over to our browser and submit the form with an uploaded file we should see the note being created successfully. And the app being redirected to the homepage. Next up we are going to allow users to see a list of the notes they've created. + +### Troubleshooting Tips + +_Sept 2020_ + +No useful HTTP error codes will show up in the error alert message, you’ll simply get “Network Error”. Look in Chrome dev tools > Network tab as you’re saving the note. + +* Forgetting to enable CORS 6 on your S3 bucket will result in `403 Forbidden`. +* You **can** pick a S3 bucket region that is different from your Cognito or API gateway. The upload will still work as long as your config.js is correct. +* I didn’t know if the above was true so I tried making a new S3 bucket in a matching region, and thought that “Copy settings from an existing bucket” would copy the CORS configuration too. Surprise! It does not. +* Setting your S3 region incorrectly in config.js results in a 301 Moved Permanently. Not super helpful :expressionless: +* Deploying your backend does result in the creation of yet another S3 bucket with a long name like “notes-app-api-prod-serverlessdeploymentbucket-ab46blaq2”. I’ve never touched this bucket and I do not reference it in my config.js or my IAM policy. +* The tutorial’s current IAM policy here 6 works for me as of Sept 2020. +* It wasn’t obvious to me how to edit the Cognito policy after creating it: IAM > Roles (under IAM Resources) > “Cognito_YourpoolnameAuth_Role” > dropdown arrow next to “oneClick_Cognito_YourpoolnameAuth_Role_#########” > Edit Policy + +Thanks [sometimescasey](https://discourse.sst.dev/u/sometimescasey){:target="_blank"} for these tips! diff --git a/_chapters/use-the-redirect-routes.md b/_chapters/use-the-redirect-routes.md index bec5f5d09..7b033f50b 100644 --- a/_chapters/use-the-redirect-routes.md +++ b/_chapters/use-the-redirect-routes.md @@ -11,18 +11,11 @@ ref: use-the-redirect-routes Now that we created the `AuthenticatedRoute` and `UnauthenticatedRoute` in the last chapter, let's use them on the containers we want to secure. -{%change%} First import them in the header of `src/Routes.js`. +{%change%} First, we switch to our new redirect routes. -```js -import AuthenticatedRoute from "./components/AuthenticatedRoute"; -import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; -``` - -Next, we simply switch to our new redirect routes. - -So the following routes in `src/Routes.js` would be affected. +So the following routes in `src/Routes.tsx` would be affected. -```jsx +```tsx } /> } /> } /> @@ -32,7 +25,7 @@ So the following routes in `src/Routes.js` would be affected. {%change%} They should now look like so: -```jsx +```tsx ``` +{%change%} Then import them in the header of `src/Routes.tsx`. + +```tsx +import AuthenticatedRoute from "./components/AuthenticatedRoute"; +import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; +``` And now if we tried to load a note page while not logged in, we would be redirected to the login page with a reference to the note page. diff --git a/_chapters/what-does-this-guide-cover.md b/_chapters/what-does-this-guide-cover.md index da6cc677c..c16dc4ac6 100644 --- a/_chapters/what-does-this-guide-cover.md +++ b/_chapters/what-does-this-guide-cover.md @@ -8,7 +8,7 @@ ref: what-does-this-guide-cover comments_id: what-does-this-guide-cover/83 --- -To step through the major concepts involved in building web applications, we are going to be building a simple note taking app called [**Scratch**]({{ site.demo_url }}). +To step through the major concepts involved in building web applications, we are going to be building a simple note taking app called [**Scratch**]({{ site.demo_url }}){:target="_blank"}. However, unlike most tutorials out there, our goal is to go into the details of what it takes to build a full-stack application for production. @@ -16,7 +16,7 @@ However, unlike most tutorials out there, our goal is to go into the details of The demo app is a single page application powered by a serverless API written completely in JavaScript. -[![Completed app desktop screenshot](/assets/completed-app-desktop.png)]({{ site.demo_url }}) +[![Completed app desktop screenshot](/assets/completed-app-desktop.png)]({{ site.demo_url }}){:target="_blank"} ![Completed app mobile screenshot](/assets/completed-app-mobile.png){: width="432" } @@ -36,43 +36,44 @@ It is a relatively simple application but we are going to address the following #### Demo Source -Here is the complete source of the app we'll be building. We recommend bookmarking it and use it as a reference. +Here is the complete source of the app we will be building. We recommend bookmarking it and use it as a reference. -- [**Demo source**]({{ site.sst_demo_repo }}) +- [**Demo source**]({{ site.sst_demo_repo }}){:target="_blank"} -We'll be using the AWS Platform to build it. We might expand further and cover a few other platforms but we figured the AWS Platform would be a good place to start. +We will be using the AWS Platform to build it. We might expand further and cover a few other platforms but we figured the AWS Platform would be a good place to start. ### Technologies & Services -We'll be using the following set of technologies and services to build our serverless application. - -- [Lambda][Lambda] & [API Gateway][APIG] for our serverless API -- [DynamoDB][DynamoDB] for our database -- [Cognito][Cognito] for user authentication and securing our APIs -- [S3][S3] for hosting our app and file uploads -- [CloudFront][CF] for serving out our app -- [Route 53][R53] for our domain -- [Certificate Manager][CM] for SSL -- [CloudWatch][CloudWatch] for Lambda and API access logs -- [React.js][React] for our single page app -- [React Router][RR] for routing -- [Bootstrap][Bootstrap] for the UI Kit -- [Stripe][Stripe] for processing credit card payments -- [Seed][Seed] for automating serverless deployments -- [Netlify][Netlify] for automating React deployments -- [GitHub][GitHub] for hosting our project repos -- [Sentry][Sentry] for error reporting +We will be using the following set of technologies and services to build our serverless application. + +- [Bootstrap][Bootstrap]{:target="_blank"} for the UI Kit +- [Certificate Manager][CM]{:target="_blank"} for SSL +- [CloudFront][CF]{:target="_blank"} for serving out our app +- [CloudWatch][CloudWatch]{:target="_blank"} for Lambda and API access logs +- [Cognito][Cognito]{:target="_blank"} for user authentication and securing our APIs +- [DynamoDB][DynamoDB]{:target="_blank"} for our database +- [GitHub][GitHub]{:target="_blank"} for hosting our project repos +- [Lambda][Lambda]{:target="_blank"} & [API Gateway][APIG]{:target="_blank"} for our serverless API +- [Netlify][Netlify]{:target="_blank"} for automating React deployments +- [React Router][RR]{:target="_blank"} for routing +- [React.js][React]{:target="_blank"} for our single page app +- [Route 53][R53]{:target="_blank"} for our domain +- [S3][S3]{:target="_blank"} for hosting our app and file uploads +- [Seed][Seed]{:target="_blank"} for automating serverless deployments +- [Sentry][Sentry]{:target="_blank"} for error reporting +- [Stripe][Stripe]{:target="_blank"} for processing credit card payments We are going to be using the **free tiers** for the above services. So you should be able to sign up for them for free. This of course does not apply to purchasing a new domain to host your app. Also for AWS, you are required to put in a credit card while creating an account. So if you happen to be creating resources above and beyond what we cover in this tutorial, you might end up getting charged. -While the list above might look daunting, we are trying to ensure that upon completing the guide you'll be ready to build **real-world**, **secure**, and **fully-functional** web apps. And don't worry we'll be around to help! +While the list above might look daunting, we are trying to ensure that upon completing the guide you will be ready to build **real-world**, **secure**, and **fully-functional** web apps. And don't worry we will be around to help! ### Requirements You just need a couple of things to work through this guide: -- [Node v12+ and NPM v6+](https://nodejs.org/en/) installed on your machine. -- A free [GitHub account](https://github.com/join). +- [Node v18+](https://nodejs.org/en/){:target="_blank"} installed on your machine. +- [PNPM v8+](https://pnpm.io/){:target="_blank"} installed on your machine. +- A free [GitHub account](https://github.com/join){:target="_blank"} . - And basic knowledge of how to use the command line. ### How This Guide Is Structured @@ -93,7 +94,7 @@ The guide is split roughly into a couple of parts: 3. **Using Serverless Framework** - The main part of the guide uses [**SST**]({{ site.sst_github_repo }}). But we also cover building the same app using [Serverless Framework](https://github.com/serverless/serverless). This is an optional section and is meant for folks trying to learn Serverless Framework. + The main part of the guide uses [**SST**]({{ site.sst_github_repo }}){:target="_blank"} . But we also cover building the same app using [Serverless Framework](https://github.com/serverless/serverless){:target="_blank"} . This is an optional section and is meant for folks trying to learn Serverless Framework. 4. **Reference** @@ -137,22 +138,22 @@ Monitoring and debugging serverless apps: - Cover the debugging workflow for common serverless errors -We think this will give you a good foundation on building full-stack production ready serverless applications. If there are any other concepts or technologies you'd like us to cover, feel free to let us know on our [forums]({{ site.forum_url }}). +We believe this will give you a good foundation on building full-stack production ready serverless applications. If there are any other concepts or technologies you'd like us to cover, feel free to let us know on our [forums]({{ site.forum_url }}){:target="_blank"} . -[Cognito]: https://aws.amazon.com/cognito/ -[CM]: https://aws.amazon.com/certificate-manager -[R53]: https://aws.amazon.com/route53/ +[APIG]: https://aws.amazon.com/api-gateway/ +[Bootstrap]: http://getbootstrap.com/ [CF]: https://aws.amazon.com/cloudfront/ -[S3]: https://aws.amazon.com/s3/ +[CM]: https://aws.amazon.com/certificate-manager/ [CloudWatch]: https://aws.amazon.com/cloudwatch/ -[Bootstrap]: http://getbootstrap.com -[RR]: https://github.com/ReactTraining/react-router -[React]: https://facebook.github.io/react/ +[Cognito]: https://aws.amazon.com/cognito/ [DynamoDB]: https://aws.amazon.com/dynamodb/ -[APIG]: https://aws.amazon.com/api-gateway/ +[GitHub]: https://github.com/ [Lambda]: https://aws.amazon.com/lambda/ -[Stripe]: https://stripe.com -[Seed]: https://seed.run -[Netlify]: https://netlify.com -[GitHub]: https://github.com -[Sentry]: https://sentry.io +[Netlify]: https://netlify.com/ +[R53]: https://aws.amazon.com/route53/ +[RR]: https://github.com/ReactTraining/react-router/ +[React]: https://facebook.github.io/react/ +[S3]: https://aws.amazon.com/s3/ +[Seed]: https://seed.run/ +[Sentry]: https://sentry.io/ +[Stripe]: https://stripe.com/ diff --git a/_chapters/what-is-an-arn.md b/_chapters/what-is-an-arn.md index b6c1ca1d1..ea710c7f0 100644 --- a/_chapters/what-is-an-arn.md +++ b/_chapters/what-is-an-arn.md @@ -8,7 +8,7 @@ description: Amazon Resource Names (or ARNs) uniquely identify AWS resources. It comments_id: what-is-an-arn/34 --- -In the last chapter while we were looking at IAM policies we looked at how you can specify a resource using its ARN. Let's take a better look at what ARN is. +An important concept in IAM is the ARN. Here is the official definition: @@ -64,4 +64,4 @@ Finally, let's look at the common use cases for ARN. ARN is used to define which resource (S3 bucket in this case) the access is granted for. The wildcard `*` character is used here to match all resources inside the *Hello-bucket*. -Next let's configure our AWS CLI. We'll be using the info from the IAM user account we created previously. +Next, you can learn more about AWS AppSync. diff --git a/_chapters/what-is-aws-appsync.md b/_chapters/what-is-aws-appsync.md index c4f16f1f4..da6f9205c 100644 --- a/_chapters/what-is-aws-appsync.md +++ b/_chapters/what-is-aws-appsync.md @@ -258,7 +258,7 @@ In most cases, you will have to pull data intermittently with queries on demand To create a subscription, you’ll first need to create a schema type of subscription and add the AWS AppSync annotation `@aws_subscribe()` to it. -```ts +```typescript type Subscription { newTodo: Todo @aws_subscribe(mutations: ["newTodo"]) diff --git a/_chapters/what-is-aws-cdk.md b/_chapters/what-is-aws-cdk.md index fbdb1d229..15feb674c 100644 --- a/_chapters/what-is-aws-cdk.md +++ b/_chapters/what-is-aws-cdk.md @@ -1,6 +1,6 @@ --- layout: post -title: What is AWS CDK +title: What is AWS CDK? date: 2020-09-14 00:00:00 lang: en description: AWS CDK (Cloud Developer Kit) is an Infrastructure as Code tool that allows you to use modern programming languages to define and provision resources on AWS. It supports JavaScript, TypeScript, Java, .NET, and Python. @@ -8,7 +8,7 @@ ref: what-is-aws-cdk comments_id: what-is-aws-cdk/2102 --- -[AWS CDK](https://aws.amazon.com/cdk/) (Cloud Development Kit), [released in Developer Preview back in August 2018](https://aws.amazon.com/blogs/developer/aws-cdk-developer-preview/); allows you to use TypeScript, JavaScript, Java, .NET, and Python to create AWS infrastructure. +[AWS CDK](https://aws.amazon.com/cdk/){:target="_blank"} (Cloud Development Kit), [released in Developer Preview back in August 2018](https://aws.amazon.com/blogs/developer/aws-cdk-developer-preview/){:target="_blank"}; allows you to use TypeScript, JavaScript, Java, .NET, and Python to create AWS infrastructure. So for example, a CloudFormation template that creates our DynamoDB table would now look like. @@ -56,9 +56,9 @@ It's fairly straightforward. The key bit here is that even though we are using C ### CDK and SST -[SST]({{ site.sst_github_repo }}) comes with a list of [higher-level CDK constructs]({{ site.docs_url }}/constructs) designed to make it easy to build serverless apps. They are easy to get started with, but also allow you to customize them. It also comes with a local development environment that we'll be relying on through this guide. So when you run: +[SST]({{ site.sst_github_repo }}){:target="_blank"} comes with a list of [higher-level CDK constructs]({{ site.docs_url }}/constructs){:target="_blank"} designed to make it easy to build serverless apps. They are easy to get started with, but also allow you to customize them. It also comes with a local development environment that we will be relying on through this guide. So when you run: - `sst build`, it runs `cdk synth` internally -- `npm start` or `npx sst deploy`, it runs `cdk deploy` +- `pnpm start` or `pnpm exec deploy`, it runs `cdk deploy` Now we are ready to create our first SST app. diff --git a/_chapters/what-is-aws-lambda.md b/_chapters/what-is-aws-lambda.md index c4b43017b..be8b9e702 100644 --- a/_chapters/what-is-aws-lambda.md +++ b/_chapters/what-is-aws-lambda.md @@ -8,7 +8,7 @@ description: AWS Lambda is a serverless computing service provided by Amazon Web comments_id: what-is-aws-lambda/308 --- -[AWS Lambda](https://aws.amazon.com/lambda/) (or Lambda for short) is a serverless computing service provided by AWS. In this chapter we are going to be using Lambda to build our serverless application. And while we don't need to deal with the internals of how Lambda works, it's important to have a general idea of how your functions will be executed. +[AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"} (or Lambda for short) is a serverless computing service provided by AWS. In this chapter we are going to be using Lambda to build our serverless application. And while we don't need to deal with the internals of how Lambda works, it's important to have a general idea of how your functions will be executed. ### Lambda Specs @@ -22,9 +22,11 @@ Let's start by quickly looking at the technical specifications of AWS Lambda. La - Ruby 2.7 - Rust -Note that, [.NET Core 2.2 and 3.0 are supported through custom runtimes](https://aws.amazon.com/blogs/developer/announcing-amazon-lambda-runtimesupport/). +{%aside%} +Note that, [.NET Core 2.2 and 3.0 are supported through custom runtimes](https://aws.amazon.com/blogs/developer/announcing-amazon-lambda-runtimesupport/){:target="_blank"}. -[See AWS for latest information on available runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html). +[See AWS for latest information on available runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){:target="_blank"}. +{%endaside%} Each function runs inside a container with a 64-bit Amazon Linux AMI. And the execution environment has: @@ -37,15 +39,15 @@ Each function runs inside a container with a 64-bit Amazon Linux AMI. And the ex You might notice that CPU is not mentioned as a part of the container specification. This is because you cannot control the CPU directly. As you increase the memory, the CPU is increased as well. -The ephemeral disk space is available in the form of the `/tmp` directory. You can only use this space for temporary storage since subsequent invocations will not have access to this. We'll talk a bit more on the stateless nature of the Lambda functions below. +The ephemeral disk space is available in the form of the `/tmp` directory. You can only use this space for temporary storage since subsequent invocations will not have access to this. We will talk a bit more on the stateless nature of the Lambda functions below. The execution duration means that your Lambda function can run for a maximum of 900 seconds or 15 minutes. This means that Lambda isn't meant for long running processes. -The package size refers to all your code necessary to run your function. This includes any dependencies (`node_modules/` directory in case of Node.js) that your function might import. There is a limit of 250MB on the uncompressed package and a 50MB limit once it has been compressed. If you need more space, you can package your container as a Docker image which can be up to 10GB. We'll take a look at the packaging process below. +The package size refers to all your code necessary to run your function. This includes any dependencies (`node_modules/` directory in case of Node.js) that your function might import. There is a limit of 250MB on the uncompressed package and a 50MB limit once it has been compressed. If you need more space, you can package your container as a Docker image which can be up to 10GB. We will take a look at the packaging process below. ### Lambda Function -Finally here is what a Lambda function (a Node.js version) looks like. +Finally here is what a Lambda function using Node.js looks like. ![Anatomy of a Lambda Function image](/assets/anatomy-of-a-lambda-function.png) @@ -53,13 +55,13 @@ Here `myHandler` is the name of our Lambda function. The `event` object contains ### Packaging Functions -Lambda functions need to be packaged and sent to AWS. This is usually a process of compressing the function and all its dependencies and uploading it to an S3 bucket. And letting AWS know that you want to use this package when a specific event takes place. To help us with this process we use the [SST]({{ site.sst_github_repo }}). We'll go over this in detail later on in this guide. +Lambda functions need to be packaged and sent to AWS. This is usually a process of compressing the function and all its dependencies and uploading it to an S3 bucket. And letting AWS know that you want to use this package when a specific event takes place. To help us with this process we use the [SST]({{ site.sst_github_repo }}). We will go over this in detail later on in this guide. ### Execution Model The container (and the resources used by it) that runs our function is managed completely by AWS. It is brought up when an event takes place and is turned off if it is not being used. If additional requests are made while the original event is being served, a new container is brought up to serve a request. This means that if we are undergoing a usage spike, the cloud provider simply creates multiple instances of the container with our function to serve those requests. -This has some interesting implications. Firstly, our functions are effectively stateless. Secondly, each request (or event) is served by a single instance of a Lambda function. This means that you are not going to be handling concurrent requests in your code. AWS brings up a container whenever there is a new request. It does make some optimizations here. It will hang on to the container for a few minutes (5 - 15mins depending on the load) so it can respond to subsequent requests without a cold start. +This has some interesting implications. Firstly, our functions are effectively stateless. Secondly, each request (or event) is served by a single instance of a Lambda function. This means that you are not going to be handling concurrent requests in your code. AWS brings up a container whenever there is a new request. It does make some optimizations here. It will hang on to the container for a few minutes (5 - 15 mins depending on the load) so it can respond to subsequent requests without a cold start. ### Stateless Functions @@ -90,7 +92,7 @@ Note that while AWS might keep the container with your Lambda function around af Lambda comes with a very generous free tier and it is unlikely that you will go over this while working on this guide. -The Lambda free tier includes 1M free requests per month and 400,000 GB-seconds of compute time per month. Past this, it costs $0.20 per 1 million requests and $0.00001667 for every GB-seconds. The GB-seconds is based on the memory consumption of the Lambda function. You can save up to 17% by purchasing AWS Compute Savings Plans in exchange for a 1 or 3 year commitment. For further details check out the [Lambda pricing page](https://aws.amazon.com/lambda/pricing/). +The Lambda free tier includes 1M free requests per month and 400,000 GB-seconds of compute time per month. Past this, it costs $0.20 per 1 million requests and $0.00001667 for every GB-seconds. The GB-seconds is based on the memory consumption of the Lambda function. You can save up to 17% by purchasing AWS Compute Savings Plans in exchange for a 1 or 3 year commitment. For further details check out the [Lambda pricing page](https://aws.amazon.com/lambda/pricing/){:target="_blank"}. In our experience, Lambda is usually the least expensive part of our infrastructure costs. diff --git a/_chapters/what-is-iam.md b/_chapters/what-is-iam.md index 6f974771d..0931d1ab6 100644 --- a/_chapters/what-is-iam.md +++ b/_chapters/what-is-iam.md @@ -8,7 +8,7 @@ description: AWS Identity and Access Management (or IAM) is a service that helps comments_id: what-is-iam/23 --- -In the last chapter, we created an IAM user so that our AWS CLI can operate on our account without using the AWS Console. But the IAM concept is used very frequently when dealing with security for AWS services, so it is worth understanding it in a bit more detail. Unfortunately, IAM is made up of a lot of different parts and it can be very confusing for folks that first come across it. In this chapter we are going to take a look at IAM and its concepts in a bit more detail. +This Guide uses Amazon Identity and Access Management (IAM) to manage users. When we setup our AWS Account, we created our first IAM user so that our AWS CLI can operate on our account without using the AWS Console. The IAM concept serves a broader purpose. It is used very frequently when dealing with security for AWS services, so it is worth understanding it in a bit more detail. Unfortunately, IAM is made up of a lot of different parts and it can be very confusing for folks that first come across it. In this chapter we are going to take a look at IAM and its concepts in a bit more detail. Let's start with the official definition of IAM. @@ -69,7 +69,7 @@ And here is a policy that grants more granular access, only allowing retrieval o } ``` -We are using S3 resources in the above examples. But a policy looks similar for any of the AWS services. It just depends on the resource ARN for `Resource` property. An ARN is an identifier for a resource in AWS and we'll look at it in more detail in the next chapter. We also add the corresponding service actions and condition context keys in `Action` and `Condition` property. You can find all the available AWS Service actions and condition context keys for use in IAM Policies [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actionsconditions.html). Aside from attaching a policy to a user, you can attach them to a role or a group. +We are using S3 resources in the above examples. But a policy looks similar for any of the AWS services. It just depends on the resource ARN for `Resource` property. An ARN is an identifier for a resource in AWS and we'll look at it in more detail in the next chapter. We also add the corresponding service actions and condition context keys in `Action` and `Condition` property. You can find all the available AWS Service actions and condition context keys for use in IAM Policies [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actionsconditions.html){:target="_blank"}. Aside from attaching a policy to a user, you can attach them to a role or a group. ### What is an IAM Role @@ -83,7 +83,7 @@ Roles can be applied to users as well. In this case, the user is taking on the p ![IAM User with IAM Role diagram](/assets/iam/iam-user-as-iam-role.png) -You can also have a role tied to the ARN of a user from a different organization. This allows the external user to assume that role as a part of your organization. This is typically used when you have a third party service that is acting on your AWS Organization. You'll be asked to create a Cross-Account IAM Role and add the external user as a *Trust Relationship*. The *Trust Relationship* is telling AWS that the specified external user can assume this role. +You can also have a role tied to the ARN of a user from a different organization. This allows the external user to assume that role as a part of your organization. This is typically used when you have a third party service that is acting on your AWS Organization. You will be asked to create a Cross-Account IAM Role and add the external user as a *Trust Relationship*. The *Trust Relationship* is telling AWS that the specified external user can assume this role. ![External IAM User with IAM Role diagram](/assets/iam/external-user-with-iam-role.png) @@ -94,4 +94,4 @@ An IAM group is simply a collection of IAM users. You can use groups to specify ![Complete IAM Group, IAM Role, IAM User, and IAM Policy diagram](/assets/iam/complete-iam-concepts.png) -This should give you a quick idea of IAM and some of its concepts. We will be referring to a few of these in the coming chapters. Next let's quickly look at another AWS concept; the ARN. +This should give you a quick overview of IAM and some of its concepts. We will be referring to a few of these elsewhere in the guide. You should also review a related concept [AWS ARN]({% link _chapters/what-is-an-arn.md %}){:target="_blank"}. diff --git a/_chapters/what-is-infrastructure-as-code.md b/_chapters/what-is-infrastructure-as-code.md index b8171ec37..bd02dbe32 100644 --- a/_chapters/what-is-infrastructure-as-code.md +++ b/_chapters/what-is-infrastructure-as-code.md @@ -8,15 +8,15 @@ ref: what-is-infrastructure-as-code comments_id: what-is-infrastructure-as-code/161 --- -[SST]({{ site.sst_github_repo }}) converts your infrastructure code into a [CloudFormation](https://aws.amazon.com/cloudformation) template. This is a description of the infrastructure that you are trying to configure as a part of your serverless project. In our case we'll be describing Lambda functions, API Gateway endpoints, DynamoDB tables, S3 buckets, etc. +[SST]({{ site.sst_github_repo }}){:target="_blank"} converts your infrastructure code into a [CloudFormation](https://aws.amazon.com/cloudformation){:target="_blank"} template using [AWS CDK](https://aws.amazon.com/cdk/){:target="_blank"} (more on this later). This is a description of the infrastructure that you are trying to configure as a part of your serverless project. In our case we'll be describing Lambda functions, API Gateway endpoints, DynamoDB tables, S3 buckets, etc. -While you can configure this using the [AWS console](https://aws.amazon.com/console/), you'll need to do a whole lot of clicking around. It's much better to configure our infrastructure programmatically. +While you can configure this using the [AWS console](https://aws.amazon.com/console/){:target="_blank"}, you'll need to do a whole lot of clicking around. It's much better to configure our infrastructure programmatically. This general pattern is called **Infrastructure as code** and it has some massive benefits. Firstly, it allows us to simply replicate our setup with a couple of simple commands. Secondly, it is not as error prone as doing it by hand. Additionally, describing our entire infrastructure as code allows us to create multiple environments with ease. For example, you can create a dev environment where you can make and test all your changes as you work on it. And this can be kept separate from your production environment that your users are interacting with. ### AWS CloudFormation -To do this we are going to be using [AWS CloudFormation](https://aws.amazon.com/cloudformation/). CloudFormation is an AWS service that takes a template (written in JSON or YAML), and provisions your resources based on that. +To do this we are going to be using [AWS CloudFormation](https://aws.amazon.com/cloudformation/){:target="_blank"}. CloudFormation is an AWS service that takes a template (written in JSON or YAML), and provisions your resources based on that. ![How CloudFormation works](/assets/diagrams/how-cloudformation-works.png) @@ -55,4 +55,4 @@ Finally, the learning curve for CloudFormation templates can be really steep. Yo ### Introducing AWS CDK -To fix these issues, AWS launched the [AWS CDK project back in August 2018](https://aws.amazon.com/blogs/developer/aws-cdk-developer-preview/). It allows you to use modern programming languages like JavaScript or Python, instead of YAML or JSON. We'll be using CDK in the coming chapters. So let's take a quick look at how it works. +To fix these issues, AWS launched the [AWS CDK project back in August 2018](https://aws.amazon.com/blogs/developer/aws-cdk-developer-preview/){:target="_blank"}. It allows you to use modern programming languages like JavaScript or Python, instead of YAML or JSON. We'll be using CDK in the coming chapters. So let's take a quick look at how it works. diff --git a/_chapters/what-is-serverless.md b/_chapters/what-is-serverless.md index 469526865..d357784fa 100644 --- a/_chapters/what-is-serverless.md +++ b/_chapters/what-is-serverless.md @@ -24,9 +24,9 @@ For smaller companies and individual developers this can be a lot to handle. Thi Serverless computing (or serverless for short), is an execution model where the cloud provider (AWS, Azure, or Google Cloud) is responsible for executing a piece of code by dynamically allocating the resources. And only charging for the amount of resources used to run the code. The code is typically run inside stateless containers that can be triggered by a variety of events including http requests, database events, queuing services, monitoring alerts, file uploads, scheduled events (cron jobs), etc. The code that is sent to the cloud provider for execution is usually in the form of a function. Hence serverless is sometimes referred to as _"Functions as a Service"_ or _"FaaS"_. Following are the FaaS offerings of the major cloud providers: -- AWS: [AWS Lambda](https://aws.amazon.com/lambda/) -- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) -- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/) +- AWS: [AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"} +- Microsoft Azure: [Azure Functions](https://azure.microsoft.com/en-us/services/functions/){:target="_blank"} +- Google Cloud: [Cloud Functions](https://cloud.google.com/functions/){:target="_blank"} While serverless abstracts the underlying infrastructure away from the developer, servers are still involved in executing our functions. @@ -40,7 +40,7 @@ The biggest change that we are faced with while transitioning to a serverless wo Your functions are typically run inside secure (almost) stateless containers. This means that you won't be able to run code in your application server that executes long after an event has completed or uses a prior execution context to serve a request. You have to effectively assume that your function is invoked in a new container every single time. -There are some subtleties to this and we will discuss in the [What is AWS Lambda]({% link _chapters/what-is-aws-lambda.md %}) chapter. +There are some subtleties to this and we will discuss in the next chapter. ### Cold Starts @@ -48,6 +48,6 @@ Since your functions are run inside a container that is brought up on demand to The duration of cold starts depends on the implementation of the specific cloud provider. On AWS Lambda it can range from anywhere between a few hundred milliseconds to a few seconds. It can depend on the runtime (or language) used, the size of the function (as a package), and of course the cloud provider in question. Cold starts have drastically improved over the years as cloud providers have gotten much better at optimizing for lower latency times. -Aside from optimizing your functions, you can use simple tricks like a separate scheduled function to invoke your function every few minutes to keep it warm. [SST]({{ site.sst_github_repo }}), which we are going to be using in this tutorial, has a pre-built [Cron]({{ site.docs_url }}/constructs/Cron) construct to help with this. +Aside from optimizing your functions, you can use simple tricks like a separate scheduled function to invoke your function every few minutes to keep it warm. [SST]({{ site.sst_github_repo }}){:target="_blank"}, which we are going to be using in this tutorial, has a pre-built [Cron]({{ site.docs_url }}/constructs/Cron){:target="_blank"} construct to help with this. Now that we have a good idea of serverless computing, let's take a deeper look at what a Lambda function is and how your code will be executed. diff --git a/_chapters/what-is-sst.md b/_chapters/what-is-sst.md index 1d47d8b5e..eb7ead5d3 100644 --- a/_chapters/what-is-sst.md +++ b/_chapters/what-is-sst.md @@ -8,22 +8,25 @@ ref: what-is-sst comments_id: comments-for-what-is-sst/2468 --- -We are going to be using [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon API Gateway](https://aws.amazon.com/api-gateway/), and a host of other AWS services to create our application. AWS Lambda is a compute service that lets you run code without provisioning or managing servers. You pay only for the compute time you consume - there is no charge when your code is not running. But working directly with AWS Lambda, API Gateway, and the other AWS services can be a bit cumbersome. +We are going to be using [AWS Lambda](https://aws.amazon.com/lambda/){:target="_blank"}, [Amazon API Gateway](https://aws.amazon.com/api-gateway/){:target="_blank"}, and a host of other AWS services to create our application. AWS Lambda is a compute service that lets you run code without provisioning or managing servers. You pay only for the compute time you consume - there is no charge when your code is not running. But working directly with AWS Lambda, API Gateway, and the other AWS services can be a bit cumbersome. Since these services run on AWS, it can be tricky to test and debug them locally. And a big part of building serverless applications, is being able to define our infrastructure as code. This means that we want our infrastructure to be created programmatically. We don't want to have to click through the AWS Console to create our infrastructure. -To solve these issues we created the [SST]({{ site.sst_github_repo }}). +To solve these issues we created the [SST]({{ site.sst_github_repo }}){:target="_blank"}. SST makes it easy to build serverless applications by allowing developers to: -1. Define their infrastructure using [AWS CDK]({% link _chapters/what-is-aws-cdk.md %}) -2. Test their applications live using [Live Lambda Development]({{ site.docs_url }}/live-lambda-development) -3. [Set breakpoints and debug in Visual Studio Code]({{ site.docs_url }}/debugging-with-vscode) -4. [Web based dashboard]({{ site.docs_url }}/console) to manage your apps -5. [Deploy to multiple environments and regions]({{ site.docs_url }}/deploying-your-app#deploying-to-a-stage) -6. Use [higher-level constructs]({{ site.docs_url }}/packages/resources) designed specifically for serverless apps -7. Configure Lambda functions with JS and TS (using [esbuild](https://esbuild.github.io/)), Go, Python, C#, and F# +1. Define their infrastructure using AWS CDK which we will cover in a later chapter. +2. Test their applications live using [Live Lambda Development]({{ site.docs_url }}/live-lambda-development){:target="_blank"} +3. Debugging with various IDEs + - [Debugging with VS Code]({{ site.docs_url }}/live-lambda-development#debugging-with-vscode){:target="_blank"} + - [Debugging with WebStorm]({{ site.docs_url }}/live-lambda-development#debugging-with-webstorm){:target="_blank"} + - [Debugging with IntelliJ IDEA]({{ site.docs_url }}/live-lambda-development#debugging-with-intellij-idea){:target="_blank"} +4. [Web based dashboard]({{ site.docs_url }}/console){:target="_blank"} to manage your apps +5. [Deploy to multiple environments and regions]({{ site.docs_url }}/deploying-your-app#deploying-to-a-stage){:target="_blank"} +6. Use [higher-level constructs]({{ site.docs_url }}/packages/resources){:target="_blank"} designed specifically for serverless apps +7. Configure Lambda functions with JS and TS (using [esbuild](https://esbuild.github.io/){:target="_blank"}), Go, Python, C#, and F# -We also have an [alternative guide using Serverless Framework]({% link _chapters/setup-the-serverless-framework.md %}). +We also have an [alternative guide using Serverless Framework]({% link _chapters/setup-the-serverless-framework.md %}){:target="_blank"}. Before we start creating our application, let's look at the _infrastructure as code_ concept in a bit more detail. diff --git a/_chapters/wrapping-up.md b/_chapters/wrapping-up.md index 873b548ec..b09be921a 100644 --- a/_chapters/wrapping-up.md +++ b/_chapters/wrapping-up.md @@ -28,7 +28,7 @@ One final thing! You can also manage your app in production with the [SST Consol Run the following in your project root. ```bash -$ npx sst console --stage prod +$ pnpm sst console --stage prod ``` This'll allow you to connect your SST Console to your prod stage. @@ -57,7 +57,7 @@ You can even see the request logs in production. We hope what you've learned here can be adapted to fit the use case you have in mind. We are going to be covering a few other topics in the future while we keep this guide up to date. -We'd love to hear from you about your experience following this guide. Please [**fill out our survey**]({{ site.survey_url }}) or send us any comments or feedback you might have, via [email](mailto:{{ site.email }}). And [please star our repo on GitHub]({{ site.sst_github_repo }}), it really helps spread the word. +We'd love to hear from you about your experience following this guide. Please [**fill out our survey**]({{ site.survey_url }}){:target="_blank"} or send us any comments or feedback you might have, via [email](mailto:{{ site.email }}). And [please star our repo on GitHub]({{ site.sst_github_repo }}){:target="_blank"}, it really helps spread the word. Star our GitHub repo diff --git a/_config.yml b/_config.yml index f109abf0c..9e417981a 100644 --- a/_config.yml +++ b/_config.yml @@ -23,6 +23,8 @@ description_full: > baseurl: "" # the subpath of your site, e.g. /blog url: "https://sst.dev" # the base hostname & protocol for your site, e.g. http://example.com +exclude: + - .idea demo_url: "https://demo.sst.dev" github_repo: "https://github.com/AnomalyInnovations/serverless-stack-com" diff --git a/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md b/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md index 8a83fde41..7fb901663 100644 --- a/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md +++ b/_examples/how-to-add-a-custom-domain-to-a-serverless-api.md @@ -65,7 +65,7 @@ Let's start by setting up an API {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, StackContext } from "sst/constructs"; export function ExampleStack({ stack, app }: StackContext) { @@ -94,7 +94,7 @@ GET / We are also configuring a custom domain for the API endpoint. -```ts +```typescript customDomain: `${stage}.example.com`; ``` @@ -108,7 +108,7 @@ Or if you have a domain hosted on another provider, [read this to migrate it to If you already have a domain in Route 53, SST will look for a [hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) with the name set to the base domain. So for example, if your custom domain is set to `dev.example.com`, SST will look for a hosted zone called `example.com`. If you have it set under a different hosted zone, you'll need to set that explicitly. -```ts +```typescript const api = new Api(stack, "Api", { customDomain: { domainName: "dev.api.example.com", @@ -124,7 +124,7 @@ For this example, we are going to focus on the custom domain. So we are going to {%change%} Replace the `packages/functions/src/lambda.ts` with the following. -```ts +```typescript export async function main() { const response = { userId: 1, @@ -205,7 +205,7 @@ Let's make a quick change to our API. It would be good if the JSON strings are p {%change%} Replace `packages/functions/src/lambda.ts` with the following. -```ts +```typescript export async function main() { const response = { userId: 1, diff --git a/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md b/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md index dc76c0e19..6f41a4838 100644 --- a/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md +++ b/_examples/how-to-add-auth0-authentication-to-a-serverless-api.md @@ -65,7 +65,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { StackContext, Api, Cognito } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -105,7 +105,7 @@ Now let's add authentication for our serverless app. {%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `domain` and `clientId` with that of your Auth0 app. -```ts +```typescript // Create auth provider const auth = new Cognito(stack, "Auth", { identityPoolFederation: { @@ -124,7 +124,7 @@ This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/lates {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript stack.addOutputs({ ApiEndpoint: api.url, IdentityPoolId: auth.cognitoIdentityPoolId, @@ -139,7 +139,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -150,7 +150,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -317,7 +317,7 @@ Let's make a quick change to our private route and print out the caller's user i {%change%} Replace `packages/functions/src/private.ts` with the following. -```ts +```typescript import { APIGatewayProxyHandlerV2 } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2 = async (event) => { diff --git a/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md b/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md index e7b79d66f..c1d9d4c61 100644 --- a/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md +++ b/_examples/how-to-add-cognito-authentication-to-a-serverless-api.md @@ -64,7 +64,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, Cognito, StackContext } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -102,7 +102,7 @@ By default, all routes have the authorization type `AWS_IAM`. This means the cal {%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Create auth provider const auth = new Cognito(stack, "Auth", { login: ["email"], @@ -118,7 +118,7 @@ This also creates a Cognito Identity Pool which assigns IAM permissions to users {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript stack.addOutputs({ ApiEndpoint: api.url, UserPoolId: auth.userPoolId, @@ -135,7 +135,7 @@ We will create two functions, one for the public route, and one for the private {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -146,7 +146,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -278,7 +278,7 @@ Let's make a quick change to our private route to print out the caller's user id {%change%} Replace `packages/functions/src/private.ts` with the following. -```ts +```typescript import { APIGatewayProxyHandlerV2 } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2 = async (event) => { diff --git a/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md b/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md index 4002cac4e..072d98a35 100644 --- a/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md +++ b/_examples/how-to-add-facebook-authentication-to-a-serverless-api.md @@ -65,7 +65,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, Cognito, StackContext } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -105,7 +105,7 @@ Now let's add authentication for our serverless app. {%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `appId` with that of your Facebook app. -```ts +```typescript // Create auth provider const auth = new Cognito(stack, "Auth", { identityPoolFederation: { @@ -121,7 +121,7 @@ This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/lates {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript stack.addOutputs({ ApiEndpoint: api.url, IdentityPoolId: auth.cognitoIdentityPoolId, @@ -136,7 +136,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -147,7 +147,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -282,7 +282,7 @@ Let's make a quick change to our private route and print out the caller's user i {%change%} Replace `packages/functions/src/private.ts` with the following. -```ts +```typescript import { APIGatewayProxyHandlerV2 } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2 = async (event) => { diff --git a/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md b/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md index 05a8835fe..21f89c229 100644 --- a/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md +++ b/_examples/how-to-add-facebook-login-to-your-cognito-user-pool.md @@ -65,7 +65,7 @@ First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/la {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import * as cognito from "aws-cdk-lib/aws-cognito"; import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; @@ -119,7 +119,7 @@ On the left navigation bar, choose Settings and then **Basic**. {%change%} Create a `.env.local` file in the root and add your Facebook `App ID` and `App secret`. -```ts +```typescript FACEBOOK_APP_ID= FACEBOOK_APP_SECRET= ``` @@ -128,7 +128,7 @@ FACEBOOK_APP_SECRET= {%change%} Add this below the `Cognito` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Throw error if App ID & secret are not provided if (!process.env.FACEBOOK_APP_ID || !process.env.FACEBOOK_APP_SECRET) throw new Error("Please set FACEBOOK_APP_ID and FACEBOOK_APP_SECRET"); @@ -158,7 +158,7 @@ Now let's associate a Cognito domain to the user pool, which can be used for sig {%change%} Add below code in `stacks/ExampleStack.ts`. -```ts +```typescript // Create a cognito userpool domain const domain = auth.cdk.userPool.addDomain("AuthDomain", { cognitoDomain: { @@ -173,7 +173,7 @@ Note, the `domainPrefix` need to be globally unique across all AWS accounts in a {%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. -```ts +```typescript // Create a HTTP API const api = new Api(stack, "Api", { authorizers: { @@ -216,7 +216,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -227,7 +227,7 @@ export async function handler() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; export const handler: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( @@ -246,7 +246,7 @@ To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.docs {%change%} Replace the `stack.addOutputs({` call with the following. -```ts +```typescript // Create a React Static Site const site = new StaticSite(stack, "Site", { path: "packages/frontend", @@ -646,7 +646,7 @@ Stack prod-api-oauth-facebook-ExampleStack Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. -```ts +```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; diff --git a/_examples/how-to-add-facebook-login-to-your-sst-apps.md b/_examples/how-to-add-facebook-login-to-your-sst-apps.md index 1a594f9b8..e1f1077a9 100644 --- a/_examples/how-to-add-facebook-login-to-your-sst-apps.md +++ b/_examples/how-to-add-facebook-login-to-your-sst-apps.md @@ -135,7 +135,7 @@ We are going to use the [`Auth`]({{ site.docs_url }}/constructs/Auth) construct. {%change%} Add the following below the `Api` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const auth = new Auth(stack, "auth", { authenticator: { handler: "packages/functions/src/auth.handler", @@ -168,7 +168,7 @@ Now let's implement the `authenticator` function. {%change%} Add a file in `packages/functions/src/auth.ts` with the following. -```ts +```typescript import { Config } from "sst/node/config"; import { AuthHandler, FacebookAdapter } from "sst/node/auth"; @@ -206,7 +206,7 @@ To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.docs {%change%} Add the following above the `Auth` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const site = new StaticSite(stack, "site", { path: "web", buildCommand: "npm run build", @@ -367,7 +367,7 @@ First, to make creating and retrieving session typesafe, we'll start by defining {%change%} Add the following above the `AuthHandler` in `packages/functions/src/auth.ts`. -```ts +```typescript declare module "sst/node/auth" { export interface SessionTypes { user: { @@ -434,7 +434,7 @@ Then in the frontend, we will check if the URL contains the `token` query string {%change%} Add the following above the `return` in `web/src/App.jsx`. -```ts +```typescript useEffect(() => { const search = window.location.search; const params = new URLSearchParams(search); @@ -452,7 +452,7 @@ On page load, we will also check if the session token exists in the local storag {%change%} Add this above the `useEffect` we just added. -```ts +```typescript const [session, setSession] = useState(null); const getSession = async () => { @@ -498,7 +498,7 @@ And finally, when the user clicks on `Sign out`, we need to clear the session to {%change%} Add the following above the `return`. -```ts +```typescript const signOut = async () => { localStorage.removeItem("session"); setSession(null); @@ -507,7 +507,7 @@ const signOut = async () => { {%change%} Also, remember to add the imports up top. -```ts +```typescript import { useEffect, useState } from "react"; ``` @@ -533,7 +533,7 @@ We'll be using the SST [`Table`]({{ site.docs_url }}/constructs/Table) construct {%change%} Add the following above the `Api` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const table = new Table(stack, "users", { fields: { userId: "string", @@ -608,7 +608,7 @@ This is saving the `claims` we get from Facebook in our DynamoDB table. {%change%} Also add these imports up top. -```ts +```typescript import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { marshall } from "@aws-sdk/util-dynamodb"; import { Table } from "sst/node/table"; @@ -637,7 +637,7 @@ Now that the user data is stored in the database; let's create an API endpoint t {%change%} Add a file at `packages/functions/src/session.ts`. -```ts +```typescript import { Table } from "sst/node/table"; import { ApiHandler } from "sst/node/api"; import { useSession } from "sst/node/auth"; @@ -687,7 +687,7 @@ As we wait, let's update our frontend to make a request to the `/session` API to {%change%} Add the following above the `signOut` function in `web/src/App.jsx`. -```ts +```typescript const getUserInfo = async (session) => { try { const response = await fetch( @@ -860,7 +860,7 @@ Note that when we are developing locally via `sst dev`, the `IS_LOCAL` environme {%change%} Also remember to import the `StaticSite` construct up top. -```ts +```typescript import { StaticSite } from "sst/node/site"; ``` diff --git a/_examples/how-to-add-github-login-to-your-cognito-user-pool.md b/_examples/how-to-add-github-login-to-your-cognito-user-pool.md index d025dafbc..79617a5af 100644 --- a/_examples/how-to-add-github-login-to-your-cognito-user-pool.md +++ b/_examples/how-to-add-github-login-to-your-cognito-user-pool.md @@ -65,7 +65,7 @@ First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/la {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { StackContext, Api, Cognito, StaticSite } from "sst/constructs"; import * as cognito from "aws-cdk-lib/aws-cognito"; @@ -109,7 +109,7 @@ Note, we haven't yet set up GitHub OAuth with our user pool, we'll do it later. {%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. -```ts +```typescript // Create a HTTP API const api = new Api(stack, "api", { authorizers: { @@ -156,7 +156,7 @@ Let's create four functions, one handling the public route, one handling the pri {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -167,7 +167,7 @@ export async function handler() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -182,7 +182,7 @@ Requesting data from the token endpoint, it will return the following form: `acc The idea for this endpoint is to take the form data sent from AWS Cognito, forward it back to GitHub with the header `accept: application/json` for GitHub API to return back in JSON form instead of **query** form. -```ts +```typescript import fetch from "node-fetch"; import parser from "lambda-multipart-parser"; @@ -220,7 +220,7 @@ User info endpoint uses a different authorization scheme: `Authorization: token The below lambda gets the Bearer token given by Cognito and modify the header to send token authorization scheme to GitHub and adds a **sub** field into the response for Cognito to map the username. -```ts +```typescript import fetch from "node-fetch"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; @@ -253,14 +253,14 @@ Note, if you haven't created a GitHub OAuth app, follow [this tutorial](https:// ![GitHub API Credentials](/assets/examples/api-oauth-github/github-api-credentials.png) -```ts +```typescript GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= ``` {%change%} Add this below the `Auth` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Throw error if client ID & secret are not provided if (!process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET) throw new Error("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET"); @@ -304,7 +304,7 @@ Now let's associate a Cognito domain to the user pool, which can be used for sig {%change%} Add below code in `stacks/ExampleStack.ts`. -```ts +```typescript // Create a cognito userpool domain const domain = auth.cdk.userPool.addDomain("AuthDomain", { cognitoDomain: { @@ -321,7 +321,7 @@ To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.docs {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript // Create a React Static Site const site = new StaticSite(stack, "Site", { path: "packages/frontend", @@ -446,7 +446,7 @@ npm install aws-amplify {%change%} Replace `frontend/src/main.jsx` with below code. -```ts +```typescript /* eslint-disable no-undef */ import React from "react"; import ReactDOM from "react-dom"; diff --git a/_examples/how-to-add-google-authentication-to-a-serverless-api.md b/_examples/how-to-add-google-authentication-to-a-serverless-api.md index 89fd92c6a..538c7d076 100644 --- a/_examples/how-to-add-google-authentication-to-a-serverless-api.md +++ b/_examples/how-to-add-google-authentication-to-a-serverless-api.md @@ -66,7 +66,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, Cognito, StackContext } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -106,7 +106,7 @@ Now let's add authentication for our serverless app. {%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `clientId` with that of your Google API project. -```ts +```typescript // Create auth provider const auth = new Cognito(stack, "Auth", { identityPoolFederation: { @@ -125,7 +125,7 @@ This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/lates {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript stack.addOutputs({ ApiEndpoint: api.url, IdentityPoolId: auth.cognitoIdentityPoolId, @@ -140,7 +140,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -151,7 +151,7 @@ export async function handler() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -298,7 +298,7 @@ Let's make a quick change to our private route and print out the caller's user i {%change%} Replace `packages/functions/src/private.ts` with the following. -```ts +```typescript import { APIGatewayProxyHandlerV2 } from "aws-lambda"; export const handler: APIGatewayProxyHandlerV2 = async (event) => { diff --git a/_examples/how-to-add-google-login-to-your-cognito-user-pool.md b/_examples/how-to-add-google-login-to-your-cognito-user-pool.md index bd0bb706b..560996a23 100644 --- a/_examples/how-to-add-google-login-to-your-cognito-user-pool.md +++ b/_examples/how-to-add-google-login-to-your-cognito-user-pool.md @@ -65,7 +65,7 @@ First, let's create a [Cognito User Pool](https://docs.aws.amazon.com/cognito/la {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import * as cognito from "aws-cdk-lib/aws-cognito"; import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; @@ -107,14 +107,14 @@ Now let's add Google OAuth for our serverless app, to do so we need to create a ![GCP Console API Credentials](/assets/examples/api-oauth-google/gcp-console-api-credentials.png) -```ts +```typescript GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` {%change%} Add this below the `Cognito` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Throw error if client ID & secret are not provided if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) throw new Error("Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET"); @@ -143,7 +143,7 @@ Now let's associate a Cognito domain to the user pool, which can be used for sig {%change%} Add below code in `stacks/ExampleStack.ts`. -```ts +```typescript // Create a cognito userpool domain const domain = auth.cdk.userPool.addDomain("AuthDomain", { cognitoDomain: { @@ -158,7 +158,7 @@ Note, the `domainPrefix` need to be globally unique across all AWS accounts in a {%change%} Replace the `Api` definition with the following in `stacks/ExampleStacks.ts`. -```ts +```typescript // Create a HTTP API const api = new Api(stack, "Api", { authorizers: { @@ -201,7 +201,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -212,7 +212,7 @@ export async function handler() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function handler() { return { statusCode: 200, @@ -227,7 +227,7 @@ To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.docs {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript // Create a React Static Site const site = new StaticSite(stack, "Site", { path: "packages/frontend", diff --git a/_examples/how-to-add-google-login-to-your-sst-apps.md b/_examples/how-to-add-google-login-to-your-sst-apps.md index c3140315b..bf4b0c7fc 100644 --- a/_examples/how-to-add-google-login-to-your-sst-apps.md +++ b/_examples/how-to-add-google-login-to-your-sst-apps.md @@ -144,7 +144,7 @@ We are going to use the [`Auth`]({{ site.docs_url }}/constructs/Auth) construct. {%change%} Add the following below the `Api` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const auth = new Auth(stack, "auth", { authenticator: { handler: "packages/functions/src/auth.handler", @@ -171,7 +171,7 @@ Now let's implement the `authenticator` function. {%change%} Add a file in `packages/functions/src/auth.ts` with the following. -```ts +```typescript import { AuthHandler, GoogleAdapter } from "sst/node/auth"; const GOOGLE_CLIENT_ID = @@ -210,7 +210,7 @@ To deploy a React app to AWS, we'll be using the SST [`StaticSite`]({{ site.docs {%change%} Add the following above the `Auth` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const site = new StaticSite(stack, "Site", { path: "web", buildCommand: "npm run build", @@ -371,7 +371,7 @@ First, to make creating and retrieving session typesafe, we'll start by defining {%change%} Add the following above the `AuthHandler` in `packages/functions/src/auth.ts`. -```ts +```typescript declare module "sst/node/auth" { export interface SessionTypes { user: { @@ -437,7 +437,7 @@ Then in the frontend, we will check if the URL contains the `token` query string {%change%} Add the following above the `return` in `web/src/App.jsx`. -```ts +```typescript useEffect(() => { const search = window.location.search; const params = new URLSearchParams(search); @@ -455,7 +455,7 @@ On page load, we will also check if the session token exists in the local storag {%change%} Add this above the `useEffect` we just added. -```ts +```typescript const [session, setSession] = useState(null); const getSession = async () => { @@ -501,7 +501,7 @@ And finally, when the user clicks on `Sign out`, we need to clear the session to {%change%} Add the following above the `return`. -```ts +```typescript const signOut = async () => { localStorage.removeItem("session"); setSession(null); @@ -510,7 +510,7 @@ const signOut = async () => { {%change%} Also, remember to add the imports up top. -```ts +```typescript import { useEffect, useState } from "react"; ``` @@ -536,7 +536,7 @@ We'll be using the SST [`Table`]({{ site.docs_url }}/constructs/Table) construct {%change%} Add the following above the `Api` construct in `stacks/ExampleStack.ts`. -```ts +```typescript const table = new Table(stack, "users", { fields: { userId: "string", @@ -610,7 +610,7 @@ This is saving the `claims` we get from Google in our DynamoDB table. {%change%} Also add these imports up top. -```ts +```typescript import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { marshall } from "@aws-sdk/util-dynamodb"; import { Table } from "sst/node/table"; @@ -639,7 +639,7 @@ Now that the user data is stored in the database; let's create an API endpoint t {%change%} Add a file at `packages/functions/src/session.ts`. -```ts +```typescript import { Table } from "sst/node/table"; import { ApiHandler } from "sst/node/api"; import { useSession } from "sst/node/auth"; @@ -689,7 +689,7 @@ As we wait, let's update our frontend to make a request to the `/session` API to {%change%} Add the following above the `signOut` function in `web/src/App.jsx`. -```ts +```typescript const getUserInfo = async (session) => { try { const response = await fetch( @@ -847,7 +847,7 @@ Note that when we are developing locally via `sst dev`, the `IS_LOCAL` environme {%change%} Also remember to import the `StaticSite` construct up top. -```ts +```typescript import { StaticSite } from "sst/node/site"; ``` diff --git a/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md b/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md index 9510f37cc..3dd9a6f27 100644 --- a/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md +++ b/_examples/how-to-add-jwt-authorization-with-auth0-to-a-serverless-api.md @@ -101,7 +101,7 @@ Let's start by setting up an API. Note that, the `issuer` option **ends with a trailing slash** (`/`). -```ts +```typescript import { StackContext, Api } from "sst/constructs"; export function ExampleStack({ stack, app }: StackContext) { @@ -153,7 +153,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -164,7 +164,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( @@ -183,7 +183,7 @@ To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.d {%change%} Replace the following in `stacks/ExampleStack.ts`: -```ts +```typescript // Show the API endpoint in the output stack.addOutputs({ ApiEndpoint: api.url, @@ -192,7 +192,7 @@ stack.addOutputs({ {%change%} With: -```ts +```typescript const site = new StaticSite(stack, "Site", { path: "packages/frontend", buildOutput: "dist", @@ -220,7 +220,7 @@ We are going to print out the resources that we created for reference. Make sure to import the `StaticSite` construct by adding below line -```ts +```typescript import { StaticSite } from "sst/constructs"; ``` @@ -545,7 +545,7 @@ A note on these environments. SST is simply deploying the same app twice using t Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. -```ts +```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; diff --git a/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md b/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md index f7e3df1e0..ceebb9e43 100644 --- a/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md +++ b/_examples/how-to-add-jwt-authorization-with-cognito-user-pool-to-a-serverless-api.md @@ -64,7 +64,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, Cognito, StackContext, StaticSite } from "sst/constructs"; export function ExampleStack({ stack, app }: StackContext) { @@ -125,7 +125,7 @@ Let's create two functions, one for the public route, and one for the private ro {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -136,7 +136,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript import { APIGatewayProxyHandlerV2WithJWTAuthorizer } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2WithJWTAuthorizer = async ( @@ -155,7 +155,7 @@ To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.d {%change%} Replace the following in `stacks/ExampleStack.ts`: -```ts +```typescript // Show the API endpoint in the output stack.addOutputs({ ApiEndpoint: api.url, @@ -166,7 +166,7 @@ stack.addOutputs({ {%change%} With: -```ts +```typescript const site = new StaticSite(stack, "Site", { path: "frontend", environment: { @@ -194,7 +194,7 @@ We are going to print out the resources that we created for reference. Make sure to import the `StaticSite` construct by adding below line -```ts +```typescript import { StaticSite } from "sst/constructs"; ``` @@ -659,7 +659,7 @@ A note on these environments. SST is simply deploying the same app twice using t Note, if you get any error like `'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js` replace `vite.config.js` with below code. -```ts +```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; diff --git a/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md b/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md index e55c72916..bc6349078 100644 --- a/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md +++ b/_examples/how-to-add-twitter-authentication-to-a-serverless-api.md @@ -66,7 +66,7 @@ Let's start by setting up an API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, Cognito, StackContext } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -106,7 +106,7 @@ Now let's add authentication for our serverless app. {%change%} Add this below the `Api` definition in `stacks/ExampleStack.ts`. Make sure to replace the `consumerKey` and `consumerSecret` with that of your Twitter app. -```ts +```typescript // Create auth provider const auth = new Cognito(stack, "Auth", { identityPoolFederation: { @@ -125,7 +125,7 @@ This creates a [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/lates {%change%} Replace the `stack.addOutputs` call with the following. -```ts +```typescript stack.addOutputs({ ApiEndpoint: api.url, IdentityPoolId: auth.cognitoIdentityPoolId, @@ -140,7 +140,7 @@ Let's create two functions, one handling the public route, and the other for the {%change%} Add a `packages/functions/src/public.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -151,7 +151,7 @@ export async function main() { {%change%} Add a `packages/functions/src/private.ts`. -```ts +```typescript export async function main() { return { statusCode: 200, @@ -319,7 +319,7 @@ Let's make a quick change to our private route and print out the caller's user i {%change%} Replace `packages/functions/src/private.ts` with the following. -```ts +```typescript import { APIGatewayProxyHandlerV2 } from "aws-lambda"; export const main: APIGatewayProxyHandlerV2 = async (event) => { diff --git a/_examples/how-to-automatically-resize-images-with-serverless.md b/_examples/how-to-automatically-resize-images-with-serverless.md index cf9b2be0a..a029c61a7 100644 --- a/_examples/how-to-automatically-resize-images-with-serverless.md +++ b/_examples/how-to-automatically-resize-images-with-serverless.md @@ -72,7 +72,7 @@ Let's start by creating a bucket. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Bucket, StackContext } from "sst/constructs"; import * as lambda from "aws-cdk-lib/aws-lambda"; @@ -325,7 +325,7 @@ $ npx sst remove --stage prod Note that, by default resources like the S3 bucket are not removed automatically. To do so, you'll need to explicitly set it. -```ts +```typescript import * as cdk from "aws-cdk-lib"; const bucket = new Bucket(stack, "Bucket", { diff --git a/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md b/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md index 9e693c1d1..e494b23cc 100644 --- a/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md +++ b/_examples/how-to-create-a-crud-api-with-serverless-using-dynamodb.md @@ -64,7 +64,7 @@ An SST app is made up of two parts. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, StackContext, Table } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -91,7 +91,7 @@ Now let's add the API. {%change%} Add this after the `Table` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Create the HTTP API const api = new Api(stack, "Api", { defaults: { @@ -135,7 +135,7 @@ Let's turn towards the functions that'll be powering our API. Starting with the {%change%} Add the following to `packages/functions/src/create.ts`. -```ts +```typescript import * as uuid from "uuid"; import { DynamoDB } from "aws-sdk"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; @@ -181,7 +181,7 @@ Next, let's write the function that'll fetch all our notes. {%change%} Add the following to `packages/functions/src/list.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -214,7 +214,7 @@ We'll do something similar for the function that gets a single note. {%change%} Create a `packages/functions/src/get.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; import { Table } from "sst/node/table"; @@ -248,7 +248,7 @@ Now let's update our notes. {%change%} Add a `packages/functions/src/update.ts` with: -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; import { Table } from "sst/node/table"; @@ -291,7 +291,7 @@ To complete the CRUD operations, let's delete the note. {%change%} Add this to `packages/functions/src/delete.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; import { Table } from "sst/node/table"; @@ -407,7 +407,7 @@ Let's make a quick change to test our Live Lambda Development environment. We wa {%change%} Replace the `return` statement in `packages/functions/src/gets.ts` with: -```ts +```typescript return results.Item ? { statusCode: 200, diff --git a/_examples/how-to-create-a-flutter-app-with-serverless.md b/_examples/how-to-create-a-flutter-app-with-serverless.md index 37722ae12..c3ab17c9c 100644 --- a/_examples/how-to-create-a-flutter-app-with-serverless.md +++ b/_examples/how-to-create-a-flutter-app-with-serverless.md @@ -73,7 +73,7 @@ We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable a {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { StackContext, Table, Api } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -99,7 +99,7 @@ Now let's add the API. {%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Create the HTTP API const api = new Api(stack, "Api", { defaults: { @@ -129,7 +129,7 @@ Our API is powered by a Lambda function. In the function we'll read from our Dyn {%change%} Replace `packages/functions/src/lambda.ts` with the following. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -365,7 +365,7 @@ Let's update our table with the clicks. {%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. -```ts +```typescript const putParams = { TableName: Table.Connections.tableName, Key: { diff --git a/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md b/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md index f67316db8..7f40c4061 100644 --- a/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md +++ b/_examples/how-to-create-a-gatsbyjs-app-with-serverless.md @@ -72,7 +72,7 @@ We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable a {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, StaticSite, StackContext, Table } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -98,7 +98,7 @@ Now let's add the API. {%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Create the HTTP API const api = new Api(stack, "Api", { defaults: { @@ -128,7 +128,7 @@ To deploy a Gatsby app to AWS, we'll be using the SST [`StaticSite`]({{ site.doc {%change%} Replace the following in `stacks/ExampleStack.ts`: -```ts +```typescript // Show the API endpoint in the output stack.addOutputs({ ApiEndpoint: api.url, @@ -137,7 +137,7 @@ stack.addOutputs({ {%change%} With: -```ts +```typescript const site = new StaticSite(stack, "GatsbySite", { path: "frontend", buildOutput: "public", @@ -162,7 +162,7 @@ We are also setting up a [build time Gatsby environment variable](https://www.ga You can also optionally configure a custom domain. -```ts +```typescript // Deploy our Gatsby app const site = new StaticSite(stack, "GatsbySite", { // ... @@ -178,7 +178,7 @@ Our API is powered by a Lambda function. In the function we'll read from our Dyn {%change%} Replace `packages/functions/src/lambda.ts` with the following. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -357,7 +357,7 @@ Let's update our table with the clicks. {%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. -```ts +```typescript const putParams = { TableName: Table.Counter.tableName, Key: { diff --git a/_examples/how-to-create-a-nextjs-app-with-serverless.md b/_examples/how-to-create-a-nextjs-app-with-serverless.md index c8b857db8..541beb8fc 100644 --- a/_examples/how-to-create-a-nextjs-app-with-serverless.md +++ b/_examples/how-to-create-a-nextjs-app-with-serverless.md @@ -93,7 +93,7 @@ To support file uploads in our app, we need an S3 bucket. Let's add that. {%change%} Add the following above our `NextjsSite` definition in the `sst.config.ts`. -```ts +```typescript const bucket = new Bucket(stack, "public"); ``` @@ -129,7 +129,7 @@ Now to let our users upload files in our Next.js app we need to start by generat {%change%} Add this to `pages/index.ts` above the `Home` component. -```ts +```typescript export async function getServerSideProps() { const command = new PutObjectCommand({ ACL: "public-read", @@ -152,7 +152,7 @@ $ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner {%change%} And add these to the imports. -```ts +```typescript import crypto from "crypto"; import { Bucket } from "sst/node/bucket"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; diff --git a/_examples/how-to-create-a-reactjs-app-with-serverless.md b/_examples/how-to-create-a-reactjs-app-with-serverless.md index 7879a2300..72eb432c7 100644 --- a/_examples/how-to-create-a-reactjs-app-with-serverless.md +++ b/_examples/how-to-create-a-reactjs-app-with-serverless.md @@ -72,7 +72,7 @@ We'll be using [Amazon DynamoDB](https://aws.amazon.com/dynamodb/); a reliable a {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, StaticSite, StackContext, Table } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -98,7 +98,7 @@ Now let's add the API. {%change%} Add this below the `Table` definition in `stacks/ExampleStack.ts`. -```ts +```typescript // Create the HTTP API const api = new Api(stack, "Api", { defaults: { @@ -128,7 +128,7 @@ To deploy a React.js app to AWS, we'll be using the SST [`StaticSite`]({{ site.d {%change%} Replace the following in `stacks/ExampleStack.ts`: -```ts +```typescript // Show the API endpoint in the output stack.addOutputs({ ApiEndpoint: api.url, @@ -137,7 +137,7 @@ stack.addOutputs({ {%change%} With: -```ts +```typescript // Deploy our React app const site = new StaticSite(stack, "ReactSite", { path: "packages/frontend", @@ -161,7 +161,7 @@ We are also setting up a [build time React environment variable](https://create- You can also optionally configure a custom domain. -```ts +```typescript // Deploy our React app const site = new StaticSite(stack, "ReactSite", { // ... @@ -177,7 +177,7 @@ Our API is powered by a Lambda function. In the function we'll read from our Dyn {%change%} Replace `packages/functions/src/lambda.ts` with the following. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -374,7 +374,7 @@ Let's update our table with the clicks. {%change%} Add this above the `return` statement in `packages/functions/src/lambda.ts`. -```ts +```typescript const putParams = { TableName: Table.Counter.tableName, Key: { diff --git a/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md b/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md index 611484a05..55d4f61f3 100644 --- a/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md +++ b/_examples/how-to-create-a-rest-api-in-golang-with-serverless.md @@ -52,7 +52,7 @@ Let's start by setting up the routes for our API. {%change%} Add the following below the `config` function in the `sst.config.ts`. -```ts +```typescript stacks(app) { app.setDefaultFunctionProps({ runtime: "go1.x", diff --git a/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md b/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md index 56cba7e71..ea8d6cacc 100644 --- a/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md +++ b/_examples/how-to-create-a-rest-api-in-typescript-with-serverless.md @@ -64,7 +64,7 @@ Let's start by setting up the routes for our API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { api, stackcontext } from "sst/constructs"; export function examplestack({ stack }: stackcontext) { @@ -100,7 +100,7 @@ For this example, we are not using a database. We'll look at that in detail in a {%change%} Let's add a file that contains our notes in `src/notes.ts`. -```ts +```typescript interface Note { noteId: string; userId: string; @@ -132,7 +132,7 @@ Now add the code for our first endpoint. {%change%} Add a `src/list.ts`. -```ts +```typescript import { APIGatewayProxyResult } from "aws-lambda"; import notes from "./notes"; @@ -152,7 +152,7 @@ Note that this function need to be `async` to be invoked by AWS Lambda. Even tho {%change%} Add the following to `src/get.ts`. -```ts +```typescript import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import notes from "./notes"; @@ -181,7 +181,7 @@ Here we are checking if we have the requested note. If we do, we respond with it {%change%} Add the following to `src/update.ts`. -```ts +```typescript import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import notes from "./notes"; @@ -280,7 +280,7 @@ Let's make a quick change to our API. It would be good if the JSON strings are p {%change%} Replace `src/list.ts` with the following. -```ts +```typescript import { APIGatewayProxyResult } from "aws-lambda"; import notes from "./notes"; diff --git a/_examples/how-to-create-a-rest-api-with-serverless.md b/_examples/how-to-create-a-rest-api-with-serverless.md index b2964a17e..bc506c913 100644 --- a/_examples/how-to-create-a-rest-api-with-serverless.md +++ b/_examples/how-to-create-a-rest-api-with-serverless.md @@ -64,7 +64,7 @@ Let's start by setting up the routes for our API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { Api, StackContext } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -100,7 +100,7 @@ For this example, we are not using a database. We'll look at that in detail in a {%change%} Let's add a file that contains our notes in `packages/core/src/notes.ts`. -```ts +```typescript export default { id1: { noteId: "id1", @@ -123,7 +123,7 @@ Now add the code for our first endpoint. {%change%} Add a `packages/functions/src/list.ts`. -```ts +```typescript import notes from "@rest-api/core/notes"; export async function handler() { @@ -142,7 +142,7 @@ Note that this function need to be `async` to be invoked by AWS Lambda. Even tho {%change%} Add the following to `packages/functions/src/get.ts`. -```ts +```typescript import notes from "@rest-api/core/notes"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; @@ -166,7 +166,7 @@ Here we are checking if we have the requested note. If we do, we respond with it {%change%} Add the following to `packages/functions/src/update.ts`. -```ts +```typescript import notes from "@rest-api/core/notes"; import { APIGatewayProxyHandlerV2 } from "aws-lambda"; @@ -266,7 +266,7 @@ Let's make a quick change to our API. It would be good if the JSON strings are p {%change%} Replace `packages/functions/src/list.ts` with the following. -```ts +```typescript import notes from "@rest-api/core/notes"; export async function handler() { diff --git a/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md b/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md index bb43497ab..c13e56cb1 100644 --- a/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md +++ b/_examples/how-to-create-a-serverless-graphql-api-with-aws-appsync.md @@ -72,7 +72,7 @@ Let's start by defining our AppSync API. {%change%} Replace the `stacks/ExampleStack.ts` with the following. -```ts +```typescript import { StackContext, Table, AppSyncApi } from "sst/constructs"; export function ExampleStack({ stack }: StackContext) { @@ -154,7 +154,7 @@ Let's also add a type for our note object. {%change%} Add the following to a new file in `packages/functions/src/graphql/Note.ts`. -```ts +```typescript type Note = { id: string; content: string; @@ -169,7 +169,7 @@ To start with, let's create the Lambda function that'll be our AppSync data sour {%change%} Replace `packages/functions/src/main.ts` with the following. -```ts +```typescript import Note from "./graphql/Note"; import listNotes from "./graphql/listNotes"; import createNote from "./graphql/createNote"; @@ -215,7 +215,7 @@ Starting with the one that'll create a note. {%change%} Add a file to `packages/functions/src/graphql/createNote.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; import Note from "./Note"; @@ -248,7 +248,7 @@ Next, let's write the function that'll fetch all our notes. {%change%} Add the following to `packages/functions/src/graphql/listNotes.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -275,7 +275,7 @@ We'll do something similar for the function that gets a single note. {%change%} Create a `packages/functions/src/graphql/getNoteById.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; import Note from "./Note"; @@ -304,7 +304,7 @@ Now let's update our notes. {%change%} Add a `packages/functions/src/graphql/updateNote.ts` with: -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; import Note from "./Note"; @@ -334,7 +334,7 @@ To complete all the operations, let's delete the note. {%change%} Add this to `packages/functions/src/graphql/deleteNote.ts`. -```ts +```typescript import { DynamoDB } from "aws-sdk"; import { Table } from "sst/node/table"; @@ -470,7 +470,7 @@ You'll notice a couple of things. Firstly, the note we created is still there. T {%change%} Let's fix our `packages/functions/src/graphql/deleteNote.ts` by un-commenting the query. -```ts +```typescript await dynamoDb.delete(params).promise(); ``` diff --git a/_examples/how-to-create-a-svelte-app-with-serverless.md b/_examples/how-to-create-a-svelte-app-with-serverless.md index 1e037abda..452d05781 100644 --- a/_examples/how-to-create-a-svelte-app-with-serverless.md +++ b/_examples/how-to-create-a-svelte-app-with-serverless.md @@ -40,7 +40,7 @@ $ npm install This will detect that you are trying to configure a Svelte app. It'll add a `sst.config.ts` and a couple of packages to your `package.json`. -```ts +```typescript import type { SSTConfig } from "sst"; import { Cron, Bucket, SvelteKitSite } from "sst/constructs"; @@ -93,7 +93,7 @@ To support file uploads in our app, we need an S3 bucket. Let's add that. {%change%} Add the following above our `SvelteKitSite` definition in the `sst.config.ts`. -```ts +```typescript const bucket = new Bucket(stack, "public"); ``` @@ -129,7 +129,7 @@ Now to let our users upload files in our Svelte app we need to start by generati {%change%} Create a `src/routes/+page.server.ts` with this. -```ts +```typescript export const load = (async () => { const command = new PutObjectCommand({ ACL: "public-read", @@ -152,7 +152,7 @@ $ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner {%change%} And add these to the imports. -```ts +```typescript import crypto from "crypto"; import { Bucket } from "sst/node/bucket"; import type { PageServerLoad } from "./$types"; @@ -177,7 +177,7 @@ Now let's add the form. {%change%} And add the upload handler as `