diff --git a/_chapters/add-an-api-to-create-a-note.md b/_chapters/add-an-api-to-create-a-note.md
index fef67ae85..784650126 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:
@@ -166,13 +185,13 @@ It includes the API endpoint that we created.
Now we are ready to test our new API.
-Head over to the **API** tab in the [SST Console]({{ site.console_url }}) and check out the new API.
+Go to the **API** tab in the [SST Console]({{ site.console_url }}){:target="_blank"} and check out the new API. You should see the `POST /notes` route. Notice the dropdown in the upper right hand corner, this allows you to select the various API endpoints. It should display something like `-notes-ApiStack / Api`
![SST Console API tab](/assets/part2/sst-console-api-tab.png)
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 7be9deb6d..fda7b8638 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:
@@ -68,7 +70,7 @@ Let's test the delete note API.
In a [previous 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`.
-In the **API** tab of the [SST Console]({{ site.console_url }}), select the `DELETE /notes/{id}` API.
+In the **API** tab of the [SST Console]({{ site.console_url }}){:target="_blank"}, select the `DELETE /notes/{id}` API.
{%change%} Set the `noteId` as the **id** and click **Send**.
diff --git a/_chapters/add-an-api-to-get-a-note.md b/_chapters/add-an-api-to-get-a-note.md
index 60f2fd584..068e14e93 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:
@@ -70,11 +74,11 @@ You should see that the API stack is being updated.
Let's test the get notes API. In the [previous 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 back to the **API** tab in the [SST Console]({{ site.console_url }}) and select the `/notes/{id}` API. You might have to refresh your console.
+Go to the **API** tab in the [SST Console]({{ site.console_url }}){:target="_blank"} 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 d29bb9665..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 verions 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 273cd35c2..de102f790 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:
@@ -69,7 +64,7 @@ You should see that the API stack is being updated.
### Test the API
-Let's test the list all notes API. Head to the **API** tab of the [SST Console]({{ site.console_url }}).
+Let's test the list all notes API. Head to the **API** tab of the [SST Console]({{ site.console_url }}){:target="_blank"}.
{%change%} Select the `/notes` API and click **Send**.
@@ -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 4607c7a92..dd03b5dc9 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.console_url }}) and select the `PUT /notes/{id}` API.
+Head to the **API** tab in the [SST Console]({{ site.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"}
-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 cfd3ea078..0bfe3fb11 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.console_url }}).
+You'll also see our new User Pool if you head over to the **Cognito** tab in the [SST Console]({{ site.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
);
```
+{%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 8be337b6a..4131a6d21 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,18 @@ 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\"}"
+$ pnpm dlx aws-api-gateway-cli-test
+ --username admin@example.com --password Passw0rd! --user-pool-id --app-client-id --cognito-region --invoke-url --api-gateway-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 +158,7 @@ Making API request
}
```
-It'll have created a new note for our test user in the **DynamoDB** tab of the [SST Console]({{ site.console_url }}).
+It'll have created a new note for our test user in the **DynamoDB** tab of the [SST Console]({{ site.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 2dd1957c3..a4b01400b 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 b781e0ea4..c66af593c 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
@@ -92,7 +92,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) {
@@ -141,7 +141,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,
@@ -152,7 +152,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 (
@@ -171,7 +171,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,
@@ -180,7 +180,7 @@ stack.addOutputs({
{%change%} With:
-```ts
+```typescript
const site = new StaticSite(stack, "Site", {
path: "packages/frontend",
buildOutput: "dist",
@@ -208,7 +208,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";
```
@@ -533,7 +533,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 2f7ea58f3..55eecf474 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";
@@ -135,7 +135,7 @@ Now in our function, we'll be handling resizing an image once it's uploaded.
{%change%} Add a new file at `packages/functions/src/resize.ts` with the following.
-```ts
+```typescript
import AWS from "aws-sdk";
import sharp from "sharp";
import stream from "stream";
@@ -300,7 +300,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 01f371209..66b67000e 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 3a25da247..3b097bd44 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 `