In this article we'll be looking at how we can quickly and easily build an API with TypeScript and Serverless.

To start this whole process we need to make sure that we have the Serverless Framework installed and have an AWS profile set up on our computer. If you haven't then you can check out this video on how to get that all set up.

Now we're onto creating our serverless project and API. We need to start in a terminal and run the command to create our new repo. All you need to do is to switch out the {YOUR FOLDER NAME} for the name of your folder.

serverless create --template aws-nodejs-typescript --path {YOUR FOLDER NAME}

This will create a very basic serverless project with typescript. If we open this new folder with VS-Code then we can see what the template has given us.

The main files we want to look at are the serverless.ts file and the handler.ts file.

The serverless.ts file is where the configuration for the deployment is held. This file tells the serverless framework the project name, the runtime language of the code, the list of functions and a few other configuration options. Whenever we want to change the architecture of our project this is the file we'll be working in.

The next file is the handler.ts file. Here we have the example code for a lambda given to us by the template. It is very basic and just returns an API Gateway response with a message and the input event. We'll be using this later as a starting block for our own API.

Create Your Own Lambda

Now that we've seen what we get with the template it's time to add our own Lambda and API endpoint.

To start we're going to make a new folder to hold all of our lambda code and call it lambdas. This helps organise it, especially when you start getting a few different lambdas in one project.

In that new folder we're going to create our new lambda calling it getCityInfo.ts. If we open this file up we can start creating our code. We can start by copying all of the handler.ts code as a starting point.

The first thing we're going to do is to change the name of the function to handler. This is personal preference but I like naming the function that handles the event handler.

On the first line in this function we need to add some code to get the city that the user is requesting. We can get this from the url path using pathParameters.

const city = event.pathparameter?.city;

One think you may notice is the use of ?. in that declaration. That is Optional Chaining and is a really cool feature. It means if the path parameter is truthy then get the city parameter, else return undefined. This means if pathParameter was not an object, this wouldn't get the cannot read property city of undefined error that causes the node runtime to error.

Now that we have the city we need to check that the city is valid and that we have data for that city. For this we need some data. We can paste this at the bottom of the file.

The difference between this and JS is that we can create an interface to tell the system what the structure of the data must be. We have a few parameters of a city that are strings, one number and then the zipCodes is an optional parameter. This means it could be there but doesn't have to be.

interface CityData {
    name: string;
    state: string;
    description: string;
    mayor: string;
    population: number;
    zipCodes?: string;
}

const cityData: { [key: string]: CityData } = {
    newyork: {
        name: 'New York',
        state: 'New York',
        description:
            'New York City comprises 5 boroughs sitting where the Hudson River meets the Atlantic Ocean. At its core is Manhattan, a densely populated borough that’s among the world’s major commercial, financial and cultural centers. Its iconic sites include skyscrapers such as the Empire State Building and sprawling Central Park. Broadway theater is staged in neon-lit Times Square.',
        mayor: 'Bill de Blasio',
        population: 8399000,
        zipCodes: '100xx–104xx, 11004–05, 111xx–114xx, 116xx',
    },
    washington: {
        name: 'Washington',
        state: 'District of Columbia',
        description: `DescriptionWashington, DC, the U.S. capital, is a compact city on the Potomac River, bordering the states of Maryland and Virginia. It’s defined by imposing neoclassical monuments and buildings – including the iconic ones that house the federal government’s 3 branches: the Capitol, White House and Supreme Court. It's also home to iconic museums and performing-arts venues such as the Kennedy Center.`,
        mayor: 'Muriel Bowser',
        population: 705549,
    },
    seattle: {
        name: 'Seattle',
        state: 'Washington',
        description: `DescriptionSeattle, a city on Puget Sound in the Pacific Northwest, is surrounded by water, mountains and evergreen forests, and contains thousands of acres of parkland. Washington State’s largest city, it’s home to a large tech industry, with Microsoft and Amazon headquartered in its metropolitan area. The futuristic Space Needle, a 1962 World’s Fair legacy, is its most iconic landmark.`,
        mayor: 'Jenny Durkan',
        population: 744955,
    },
};

If we want to test our interface we can try adding a new property to any of the objects. TypeScript should instantly tell you that your new parameter doesn't exist on the interface. If you delete one of the required properties the same will happen. This just makes sure that you always have the correct data and objects always look exactly as expected.

Now that we have the data we can check if the user sent up the correct city request.

if (!city || !cityData[city]) {
    
}

If this statement is true then the user has done something wrong, therefore we need to return a 400 response. We could just manually type the code here but we're going to create a new apiResponses object with methods for a few of the possible API response codes.

const apiResponses = {
    _200: (body: { [key: string]: any }) => {
        return {
            statusCode: 200,
            body: JSON.stringify(body, null, 2),
        };
    },
    _400: (body: { [key: string]: any }) => {
        return {
            statusCode: 400,
            body: JSON.stringify(body, null, 2),
        };
    },
};

This just makes it much easier to reuse later in the file. You should also see that we have one property of body: { [key: string]: any }. This is stating that this function has one property of body which needs to be an object. That object can have keys that have a value of any type. Because we know that body is always going to be a string we can use JSON.stringify to make sure we return a string body.

If we add this function to our handler we get this:

export const handler: APIGatewayProxyHandler = async (event, _context) => {
    const city = event.pathParameters?.city;

    if (!city || !cityData[city]) {
        return apiResponses._400({ message: 'missing city or no data for that city' });
    }

    return apiResponses._200(cityData[city]);
};

If the user didn't pass up a city of passed up one we have no data for we return a 400 with an error message. If the data does exist then we return a 200 with a body of the data.