Add an API to Create a Note
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 the API
Replace the infra/api.ts
with the following.
import { table } from "./storage";
// Create the API
export const api = new sst.aws.ApiGatewayV2("Api", {
transform: {
route: {
handler: {
link: [table],
},
}
}
});
api.route("POST /notes", "packages/functions/src/create.main");
We are doing a couple of things of note here.
-
We are creating an API using SST’s
Api
component. It creates an Amazon API Gateway HTTP API. -
We are linking our DynamoDB table to our API using the
link
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. -
By using the
transform
prop we are telling the API that we want the given props to be applied to all the routes in our API.
Add the Function
Now let’s add the function that’ll be creating our note.
Create a new file in packages/functions/src/create.ts
with the following.
import * as uuid from "uuid";
import { Resource } from "sst";
import { APIGatewayProxyEvent } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const dynamoDb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export async function main(event: APIGatewayProxyEvent) {
let data, params;
// Request body is passed in as a JSON encoded string in 'event.body'
if (event.body) {
data = JSON.parse(event.body);
params = {
TableName: Resource.Notes.name,
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.send(new PutCommand(params));
return {
statusCode: 200,
body: JSON.stringify(params.Item),
};
} catch (error) {
let message;
if (error instanceof Error) {
message = error.message;
} else {
message = String(error);
}
return {
statusCode: 500,
body: JSON.stringify({ error: message }),
};
}
}
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. - We can access our linked DynamoDB table through
Resource.Notes.name
using the SST SDK. Here,Notes
inResource.Notes
, is the name of our Table component from the Create a DynamoDB Table in SST chapter. By doinglink: [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 to123
. 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 thecreatedAt
. - And if the DynamoDB call fails then return an error with the HTTP status code
500
.
Let’s go ahead and install the packages that we are using here.
Navigate to the functions
folder in your terminal.
$ cd packages/functions
Then, run the following in the packages/functions/
folder.
$ npm install uuid @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb
$ npm install -D @types/uuid @types/aws-lambda
- uuid generates unique ids.
- @types/aws-lambda & @types/uuid provides the TypeScript types.
- @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb allows us to talk to DynamoDB
Deploy Our Changes
If you switch over to your terminal, you will notice that your changes are being deployed.
Once complete, you should see.
+ Complete
Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com
Test the API
Now we are ready to test our new API.
Run the following in your terminal.
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"content":"Hello World","attachment":"hello.jpg"}' \
<YOUR_Api>/notes
Replace <YOUR_Api>
with the Api
from the output above. For example, our command will look like:
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"content":"Hello World","attachment":"hello.jpg"}' \
https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes
Here we are making a POST request to our create note API. We are passing in the content
and attachment
as a JSON string. In this case the attachment is a made up file name. We haven’t uploaded anything to S3 yet.
The response should look something like this.
{"userId":"123","noteId":"a46b7fe0-008d-11ec-a6d5-a1d39a077784","content":"Hello World","attachment":"hello.jpg","createdAt":1629336889054}
Make a note of the noteId
. We are going to use this newly created note in the next chapter.
Refactor Our Code
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 move this into our core
package.
Start by replacing our create.ts
with the following.
import * as uuid from "uuid";
import { Resource } from "sst";
import { Util } from "@notes/core/util";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const dynamoDb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const main = Util.handler(async (event) => {
let data = {
content: "",
attachment: "",
};
if (event.body != null) {
data = JSON.parse(event.body);
}
const params = {
TableName: Resource.Notes.name,
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.send(new PutCommand(params));
return JSON.stringify(params.Item);
});
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. - We want to centrally handle any errors in our Lambda functions.
- Finally, since all of our Lambda functions will be handling API endpoints, we want to handle our HTTP responses in one place.
Create a packages/core/src/util/index.ts
file with the following.
import { Context, APIGatewayProxyEvent } from "aws-lambda";
export module Util {
export function handler(
lambda: (evt: APIGatewayProxyEvent, context: Context) => Promise<string>
) {
return async function(event: APIGatewayProxyEvent, context: Context) {
let body: string, statusCode: number;
try {
// Run the Lambda
body = await lambda(event, context);
statusCode = 200;
} catch (error) {
statusCode = 500;
body = JSON.stringify({
error: error instanceof Error ? error.message : String(error),
});
}
// Return HTTP response
return {
body,
statusCode,
};
};
}
}
We are now using the Lambda types in core
as well. Run the following in the packages/core/
directory.
$ npm install -D @types/aws-lambda
Let’s go over this in detail.
- We are creating a
handler
function that we’ll use as a wrapper around our Lambda functions. - It takes our Lambda function as the argument.
- We then run the Lambda function in a
try/catch
block. - On success, we take 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. - Exporting the whole thing inside a
Util
module allows us import it asUtil.handler
. It also lets us put other util functions in this module in the future.
Remove Template Files
The template we are using comes with some example files that we can now remove.
Run the following from the project root.
$ rm -rf packages/core/src/example packages/functions/src/api.ts
Next, we are going to add the API to get a note given its id.
Common Issues
-
path received type undefined
Restarting
npx 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 thecatch
block. You’ll see aconsole.error
is included in ourutil/index.ts
code above. Adding logs like these can help give you insight on issues and how to resolve them.} catch (e) { // Prints the full error console.error(e); body = { error: e.message }; statusCode = 500; }
For help and discussion
Comments on this chapter