Ekekenta Odionyenfe I am a software engineer and technical writer who is proficient in server-side scripting and database setup.

Building a serverless app with TypeScript

9 min read 2545

Serverless App TypeScript

Did you know that you can write and deploy your code on the cloud without having to worry about provisioning, maintaining, and scaling the underlying infrastructure? In this tutorial, we’ll learn how to build a serverless application using TypeScript and the Serverless Framework.

The full code for the project is available at the GitHub repository. Feel free to clone the project to follow along. Let’s get started!

Table of contents

What is serverless computing?

Serverless computing is an execution paradigm for cloud computing in which the cloud provider allocates machine resources on-demand, managing servers on behalf of its clients.

The term serverless is misleading in the sense that servers still employ cloud service providers to run code for developers. The code is often executed within stateless containers that can be triggered by events like HTTP requests, queuing services, monitoring alerts, file uploads, scheduled events, database events, and more.

What is the Serverless Framework?

The Serverless Framework is an open source command-line interface (CLI) and hosted dashboard that enables comprehensive serverless application lifecycle management.

The Serverless Framework allows you to design, deploy, debug, and protect serverless applications with minimal overhead and cost, all while providing the required infrastructure resources, like AWS, Azure, Google, etc. It provides structure, automation, and support for best practices out of the box, allowing you to focus on developing sophisticated, event-driven, serverless systems made of functions and events.

The Serverless Framework has several advantages over other application frameworks. For one, it manages your code as well as your infrastructure and supports multiple languages, like Node.js, Python, Java, and more.

It has a hosted dashboard that allows you to import existing projects, track performance, troubleshoot, configure CI/CD and deployment policies, and get end-to-end serverless application lifecycle management.

What is AWS Lambda?

AWS Lambda is an event-driven, serverless computing platform from Amazon Web Services that runs code in response to events and automatically maintains the computing resources needed by that code.

For the examples in this tutorial, we’ll use AWS Lambda to run and manage our serverless functions. To get started, create an AWS free tier account. Then, follow the steps in the official serverless docs to create an IAM User and Access Key. With everything set up, let’s install the AWS CLI and set up our credentials.



Set up the AWS CLI

To create and manage resources from your terminal, the Serverless Framework needs access to your cloud provider account. To install the AWS CLI, you can follow the steps in the official docs. Then, set up your credentials by running the command below:

aws configure

The command above will prompt you for your AWS credentials. Once you add them, hit the enter button.

Project setup

With the command below, we’ll install the Serverless package globally and initialize a new serverless TypeScript project:

# Install serverless package globally
npm install -g serverless
#Initialize a new serverless project
serverless create --template aws-nodejs-typescript --path aws-serverless-typescript-api

The command above installs the serverless package, then initializes a new serverless project with the following folder structure and some boilerplate code:

aws-serverless-typescript-api
 ┣ src
 ┃ ┣ functions
 ┃ ┃ ┣ hello
 ┃ ┃ ┃ ┣ handler.ts
 ┃ ┃ ┃ ┣ index.ts
 ┃ ┃ ┃ ┣ mock.json
 ┃ ┃ ┃ ┗ schema.ts
 ┃ ┃ ┗ index.ts
 ┃ ┗ libs
 ┃ ┃ ┣ api-gateway.ts
   handler-resolver.ts
 ┃ ┃ ┗ lambda.ts
 ┣ .npmignore
┣ .nvmrc
 ┣ README.md
 ┣ package.json
 ┣ serverless.ts
 ┣ tsconfig.json
 ┗ tsconfig.paths.json

Delete the hello function folder, then install all the packages required to run the application with the command below:

npm install
#OR
yarn

Install dependencies

Let’s install the other required dependencies for our project. Run the command below to install the uuid module:

#shell
npm install uuid

The module will be used to generate random IDs for the to-do list items in our database.

Configure our project

Next, we’ll open the src folder and create a folder structure for our project. Create the following folders in the src folder:

  • Models: We’ll define our schema and connect it to our database
  • Service: We’ll create our business handler functions
  • Functions/todo: We’ll create our todos functions

After creating the folders above, you should have a new project structure like the one below:

aws-serverless-typescript-api
 ┣ src
 ┃ ┣ functions
 ┃ ┃ ┗ todo
 ┃ ┣ libs
 ┃ ┃ ┣ api-gateway.ts
 ┃ ┃ ┣ handler-resolver.ts
 ┃ ┃ ┗ lambda.ts
 ┃ ┣ model
 ┃ ┗ service
 ┣ .npmignore
 ┣ .nvmrc
 ┣ README.md
 ┣ package.json
 ┣ serverless.ts
 ┣ tsconfig.json
 ┗ tsconfig.paths.json

Now, let’s add some actual configuration in our configuration serverless.ts file. Replace the code in the serverless.ts file with the following code snippet below:

import type { AWS } from '@serverless/typescript';
import { createTodo, getTodo, getAllTodos, updateTodo, deleteTodo } from '@functions/todo';
const serverlessConfiguration: AWS = {
  service: 'aws-serverless-typescript-api',
  frameworkVersion: '3',
  plugins: ['serverless-esbuild', 'serverless-offline', 'serverless-dynamodb-local'],
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x',
    apiGateway: {
      minimumCompressionSize: 1024,
      shouldStartNameWithService: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',
    },
    iam: {
      role: {
        statements: [{
          Effect: "Allow",
          Action: [
            "dynamodb:DescribeTable",
            "dynamodb:Query",
            "dynamodb:Scan",
            "dynamodb:GetItem",
            "dynamodb:PutItem",
            "dynamodb:UpdateItem",
            "dynamodb:DeleteItem",
          ],
          Resource: "arn:aws:dynamodb:us-west-2:*:table/TodosTable",
        }],
      },
    },
  },
  // import the function via paths
  functions: { getAllTodos, createTodo, getTodo, updateTodo, deleteTodo },
  package: { individually: true },
  custom:{
    esbuild: {
      bundle: true,
      minify: false,
      sourcemap: true,
      exclude: ['aws-sdk'],
      target: 'node14',
      define: { 'require.resolve': undefined },
      platform: 'node',
      concurrency: 10,
    },
    dynamodb:{
      start:{
        port: 5000,
        inMemory: true,
        migrate: true,
      },
      stages: "dev"
    }
  },
  resources: {
    Resources: {
      TodosTable: {
        Type: "AWS::DynamoDB::Table",
        Properties: {
          TableName: "TodosTable",
          AttributeDefinitions: [{
            AttributeName: "todosId",
            AttributeType: "S",
          }],
          KeySchema: [{
            AttributeName: "todosId",
            KeyType: "HASH"
          }],
          ProvisionedThroughput: {
            ReadCapacityUnits: 1,
            WriteCapacityUnits: 1
          },

        }
      }
    }
  }
};
module.exports = serverlessConfiguration;

Let’s review the major configurations we’ve set up for our project. For one, information about our project is defined in the service section aws-serverless-typescript-api.

frameworkVersion is the version of the Serverless Framework our project is running on. Version 3 is the latest release at the time of writing.


More great articles from LogRocket:


In Plugins, we define the plugins we need to overwrite or extend the functionality of our project. We have two plugins, serverless-esbuild and serverless-offline that enable our project to run locally. serverless-dynamodb-local enables us to run DynamoDB locally.

In Provider, we configure the cloud provider used for our project. We defined some properties of the cloud provider, like the name, runtime, apiGateway, and iam statements to give our Lambda functions read and write permissions to our DynamoDB resource table.

In Resources, we add our cloudFormation resource templates for our DynamoDB database. Here, we define some properties like the tableName, AttributeDefinitions, where we specified the primary key todosId of our table, and ProvisionedThroughput, where we set the number of units our table can read and write in one second.

In Custom, we define our custom configuration. We defined port 5000 for our DynamoDB database.

Connect to DynamoDB

In the src/model folder, we’ll create an index.ts file and connect to DynamoDB with the code snippet below:

import * as AWS from "aws-sdk";
import { DocumentClient } from "aws-sdk/clients/dynamodb";

export default const dynamoDBClient = (): DocumentClient => {
  if (process.env.IS_OFFLINE) {
    return new AWS.DynamoDB.DocumentClient({
      region: "localhost",
      endpoint: "http://localhost:5000",
    });
  }
  return new AWS.DynamoDB.DocumentClient();
};

In the code snippet above, we’ve connected to our DynamoDB database using the AWS.DynamoDB.DocumentClient method. When we run our project locally, we’ll connect to the DynamoDB localhost endpoint. Then, we’ll install DynamoDB locally with the command below:

serverless dynamodb install

Create our model

In the src/model folder, we create a Todo.ts file and add the code snippet below:

export default interface Todo {
    todosId: string;
    title: string;
    description: string;
    status: boolean;
    createdAt: string;
}

In the code snippet above, we defined an interface for our todo object. All of the to-do list items saved in our TodosTable will be in that form.

Create our services

We’ll create the service for our Lambda functions by creating a service.ts file in the /src/service folder. Then, we’ll import DocumentClinet from aws-sdk, import our Todo model, and create a TodosService class with a constructor method.

We’ll inject the DocumentClient into our TodosService class, then create a global variable for our table name with the code snippet below:

import { DocumentClient } from "aws-sdk/clients/dynamodb";
import Todo from "../model/Todo";

export default class TodoServerice {

    private Tablename: string = "TodosTable2";

    constructor(private docClient: DocumentClient) { }
    ...

Then, we create the getAllTodos method, which returns a promise of all the to-do items in our database:

    ...
    async getAllTodos(): Promise<Todo[]> {
        const todos = await this.docClient.scan({
            TableName: this.Tablename,
        }).promise()
        return todos.Items as Todo[];
     }
     ...

Next, we create the createTodo method, which will allow us to add new to-do list items to our database. The handler function takes in a todo object as an argument, which is of type Todo model:

   ... 
   async createTodo(todo: Todo): Promise<Todo> {
        await this.docClient.put({
            TableName: this.Tablename,
            Item: todo
        }).promise()
        return todo as Todo;

    }
    ...

We create a getTodo method that takes in the todo ID as an argument, returning the todo with the ID on the request parameter:

   ...
    async getTodo(id: string): Promise<any> {

        const todo = await this.docClient.get({
            TableName: this.Tablename,
            Key: {
                todosId: id
            }
        }).promise()
        if (!todo.Item) {
            throw new Error("Id does not exit");
        }
        return todo.Item as Todo;

    }
    ...

To update the to-do list items in our database, we created an updateTodo method. Like the getTodo method, the updateTodo method also takes the todo id as an argument, updating the status of the todo with the ID in the request parameter:

   ...
      async updateTodo(id: string, todo: Partial<Todo>): Promise<Todo> {
        const updated = await this.docClient
            .update({
                TableName: this.Tablename,
                Key: { todosId: id },
                UpdateExpression:
                    "set #status = :status",
                ExpressionAttributeNames: {
                    "#status": "status",
                },
                ExpressionAttributeValues: {
                    ":status": true,
                },
                ReturnValues: "ALL_NEW",
            })
            .promise();
        return updated.Attributes as Todo;
    }
    ...

Next, we create a deleteTodo method, which also takes the todo idas an argument to delete the todo from our database:

 ...
    async deleteTodo(id: string): Promise<any> {
        return await this.docClient.delete({
            TableName: this.Tablename,
            Key: {
                todosId: id
            }
        }).promise();
       }
     }
    ...

In our TodosService methods, we used the DynamoDB docClientmethod to create our CRUD operations. Finally, create an index.ts file in the /src/service folder. Create and export an instance of our TodosService with the code snippet below:

import dynamoDBClient from "../model/database";
import TodoServerice from "./todosService"

const todoService = new TodoServerice(dynamoDBClient());
export default todoService;

Create our Lambda functions

Now, we’ll create our AWS Lambda functions. First, create a handlers.ts file in the /src/functions/todo folder.

From AWS Lambda, import APIGatewayProxyEvent, APIGatewayProxyResult, formatJSONResponse to return a JSON-formatted response to the client side, middyfy to handle our middlewares, v4 to generate random strings, and our todosService:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { formatJSONResponse } from '@libs/api-gateway';
import { middyfy } from '@libs/lambda';
import { v4 } from "uuid";
import todosService from '../../services'
...

Next, we’ll create our getAllTodos Lambda function. We wrap our function inside the middyfy function to perform all middleware-related operations on our function. With the code snippet below, we call the getAllTodos method, await the result, and return the todos to the client:

...
export const getAllTodos = middyfy(async (): Promise<APIGatewayProxyResult> => {
    const todos = await todosService.getAllTodos();
    return formatJSONResponse ({
        todos
    })
})
...

Next, we’ll create our own createTodo function, where we’ll listen to an APIGatewayProxyEvent to get the data in our request body. Then, we’ll call the createTodo method and pass in a todo object. We’ll get the title and description from the request body and add other details by default with the code snippet below:

...
export const createTodo = middyfy(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    try {
        const id = v4();
        const todo = await todosService.createTodo({
            todosId: id,
            title: event.body.title,
            description: event.body.description,
            createdAt: new Date().toISOString(),
            status: false
        })
        return formatJSONResponse({
            todo
        });
    } catch (e) {
        return formatJSONResponse({
            status: 500,
            message: e
        });
    }
}
...

Next, we’ll create our getTodo function to consume the getTodo resource. We’ll get the todo ID from the request parameter using the pathParameters event. Then, we’ll return the todo to the client with the code snippet below:

...
export const getTodo = middyfy(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const id = event.pathParameters.id;
    try {
        const todo = await todosService.getTodo(id)
        return formatJSONResponse({
            todo, id
        });
    } catch (e) {
        return formatJSONResponse({
            status: 500,
            message: e
        });
    }
})
...

Now, let’s create our updateTodoLambda function. Like the getTodofunction,updateTodowill get the ID of the todofrom the request parameter, then call thedeleteTodo method from our todosService class, deleting a to-do item with the following code:

...
export const updateTodo = middyfy(async (): Promise<APIGatewayProxyResult> => {
    const id = event.pathParameters.id;
    try {
        const todo = await todosService.updateTodo(id)
        return formatJSONResponse({
            todo, id
        });
    } catch (e) {
        return formatJSONResponse({
            status: 500,
            message: e
        });
    }
})
...

Next, we’ll create our deleteTodo Lambda function, which will also get the todo ID from the request parameter and call the deleteTodo method from our todosService class, deleting a to-do item:

...
export const deleteTodo = middyfy(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const id = event.pathParameters.id;
    try {
        const todo = await todosService.deleteTodo(id)
        return formatJSONResponse({
            todo, id
        });
    } catch (e) {
        return formatJSONResponse({
            status: 500,
            message: e
        });
    }
})

Next, we’ll configure and export our Lambda functions, making them available to our serverless.ts file. Create an index.ts file in the functions/todo folder and add the code snippet below:

import { handlerPath } from '@libs/handler-resolver';
export const getAllTodos = {
    handler: `${handlerPath(__dirname)}/handler.getAllTodos`,
    events: [
        {
            http: {
                method: 'get',
                path: 'todo/',
            },
        },
    ],
};

export const createTodo = {
    handler: `${handlerPath(__dirname)}/handler.createTodo`,
    events: [
        {
            http: {
                method: 'post',
                path: 'todo',
            },
        },
    ],
};

export const getTodo = {
    handler: `${handlerPath(__dirname)}/handler.getTodo`,
    events: [
        {
            http: {
                method: 'get',
                path: 'todo/{id}',
            },
        },
    ],
};

export const updateTodo = {
    handler: `${handlerPath(__dirname)}/handler.updateTodo`,
    events: [
        {
            http: {
                method: 'put',
                path: 'todo/{id}',
            },
        },
    ],
};

export const deleteTodo = {
    handler: `${handlerPath(__dirname)}/handler.deleteTodo`,
    events: [
        {
            http: {
                method: 'delete',
                path: 'todo/{id}',
            },
        },
    ],
};

In the code above, we defined the properties of our Lambda functions, including the http method and path for all our functions.

Finally, with the code snippet below, let’s configure the Lambda functions in our serverless.ts file, making them available to our Serverless Framework:

...
import { createTodo, getTodo, getAllTodos, updateTodo, deleteTodo } from '@functions/todo';

//locate the function object, e.g. function: {} Add the code below.
 functions: { getAllTodos, createTodo, getTodo, updateTodo, deleteTodo },

At this point, we’ve successfully created our AWS Lambda functions.

Test our application

Now, we’ll test our application using Insomnia. Alternately, you can use Postman. First, run the command below to start the application locally:

serverless offline start

You should see an output like the one in the screenshot below:

Test Application Insomnia Output

Add the to-do endpoint with the POST /dev/todo route:

Add Todo Endpoint Post Request

Go ahead and test the other endpoints.

Deploy our Lambda function to AWS

Now, we’ll deploy our Lambda function to AWS using the command below:

serverless deploy

Once the deployment is completed, you should see our serverless endpoints and functions in the terminal like in the screenshot below:

Serverless Endpoints Functions Terminal

Conclusion

Regardless of the type of application you want to build, the Serverless Framework can simplify the process for you. If you want to use stacks other than Node.js or TypeScript, the Serverless Framework has support for languages like Java, Go, PowerShell, C#, Python, and Ruby.

Now that you’re familiar with the Serverless Framework, you should feel free to fork the repository and add additional features on to the project. I hope you’ve learned a lot from this tutorial. If you enjoyed it or have any questions, feel free to share, comment, or reach me out on Twitter.

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Ekekenta Odionyenfe I am a software engineer and technical writer who is proficient in server-side scripting and database setup.

2 Replies to “Building a serverless app with TypeScript”

  1. This article is really helpful as a whole to understand the environment and codebase. But the code has some small issues, syntax, types or typos that at prevent it from being executed. Manual fixes had to be made in order to run the project.

    1. Lots of manual fixes starting from the beginning. Create folder “Models”, example uses “model”, file names like “services/service.ts” are actually “services/todoService”, and zero mention of “models/database”. But yea, if you’re experienced this isn’t too much of an issue. It would be incredibly confusing if this was your first touchpoint.

Leave a Reply