Das vollständige Handbuch zum Erstellen einer API mit TypeScript und AWS

In diesem Artikel werden wir uns ansehen, wie wir schnell und einfach eine API mit TypeScript und Serverless erstellen können.

Anschließend erfahren Sie, wie Sie mit aws-sdk auf andere AWS-Services zugreifen und eine automatische Übersetzungs-API erstellen.

Wenn Sie lieber zuschauen und lernen möchten, können Sie sich das folgende Video ansehen:

Beginnen

Um diesen gesamten Prozess zu starten, müssen wir sicherstellen, dass das Serverless Framework installiert und ein AWS-Profil auf unserem Computer eingerichtet ist. Wenn Sie dies nicht getan haben, können Sie sich in diesem Video ansehen, wie Sie alles einrichten können.

Wenn Sie diesem Tutorial folgen möchten, können Sie alle Schritte ausführen oder den Code hier herunterladen und den vollständigen Code eingeben.

Jetzt erstellen wir unser serverloses Projekt und unsere API. Wir müssen in einem Terminal starten und den Befehl ausführen, um unser neues Repo zu erstellen. Alles, was Sie tun müssen, ist, {YOUR FOLDER NAME}den Namen Ihres Ordners auszutauschen.

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

Dadurch wird mit TypeScript ein sehr einfaches serverloses Projekt erstellt. Wenn wir diesen neuen Ordner mit VS-Code öffnen, können wir sehen, was uns die Vorlage gegeben hat.

Die Hauptdateien, die wir betrachten möchten, sind die serverless.tsDatei und die handler.tsDatei.

In der serverless.tsDatei befindet sich die Konfiguration für die Bereitstellung. Diese Datei teilt dem serverlosen Framework den Projektnamen, die Laufzeitsprache des Codes, die Liste der Funktionen und einige andere Konfigurationsoptionen mit.

Wann immer wir die Architektur unseres Projekts ändern möchten, ist dies die Datei, in der wir arbeiten werden.

Die nächste Datei ist die handler.tsDatei. Hier haben wir den Beispielcode für ein Lambda, das uns von der Vorlage gegeben wurde. Es ist sehr einfach und gibt nur eine API-Gateway-Antwort mit einer Nachricht und dem Eingabeereignis zurück. Wir werden dies später als Startblock für unsere eigene API verwenden.

Erstellen Sie Ihr eigenes Lambda

Nachdem wir gesehen haben, was wir mit der Vorlage erhalten, ist es Zeit, unseren eigenen Lambda und API-Endpunkt hinzuzufügen.

Zu Beginn erstellen wir einen neuen Ordner, in dem unser gesamter Lambda-Code gespeichert ist, und rufen ihn auf lambdas. Dies hilft bei der Organisation, insbesondere wenn Sie in einem Projekt einige verschiedene Lambdas erhalten.

In diesem neuen Ordner erstellen wir unser neues Lambda, das es nennt getCityInfo.ts. Wenn wir diese Datei öffnen, können wir mit der Erstellung unseres Codes beginnen. Wir können beginnen, indem wir den gesamten handler.tsCode als Ausgangspunkt kopieren .

Als erstes ändern wir den Namen der Funktion in handler. Dies ist eine persönliche Präferenz, aber ich mag es, die Funktion zu benennen, die den Ereignishandler behandelt.

In der ersten Zeile dieser Funktion müssen wir Code hinzufügen, um die Stadt zu erhalten, die der Benutzer anfordert. Wir können dies aus dem URL-Pfad mit abrufen pathParameters.

const city = event.pathparameter?.city;

Eine Sache, die Sie möglicherweise bemerken, ist die Verwendung ?.in dieser Erklärung. Das ist optionale Verkettung und eine wirklich coole Funktion. ich

t bedeutet, wenn der Pfadparameter wahr ist, erhalten Sie den Stadtparameter, andernfalls geben Sie undefiniert zurück. Das heißt, wenn pathParameteres sich nicht um ein Objekt handelt , wird der Fehler, durch den die Knotenlaufzeit fehlerhaft wird, nicht angezeigt.cannot read property city of undefined

Nachdem wir die Stadt haben, müssen wir überprüfen, ob die Stadt gültig ist und ob wir Daten für diese Stadt haben. Dafür benötigen wir einige Daten. Wir können den folgenden Code verwenden und diesen am Ende der Datei einfügen.

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, }, };

Der Unterschied zwischen diesem und JavaScript besteht darin, dass wir eine Schnittstelle erstellen können , um dem System mitzuteilen, wie die Struktur der Daten sein muss. Dies fühlt sich am Anfang wie zusätzliche Arbeit an, wird aber später dazu beitragen, alles einfacher zu machen.

Innerhalb unserer Schnittstelle definieren wir die Schlüssel des Stadtobjekts; Einige davon sind Zeichenfolgen, eine Zahl und dann zipCodeseine optionale Eigenschaft. Das heißt, es könnte da sein, muss es aber nicht sein.

Wenn wir unsere Benutzeroberfläche testen möchten, können wir versuchen, einer der Städte in unseren Stadtdaten eine neue Eigenschaft hinzuzufügen.

TypeScript sollte Ihnen sofort mitteilen, dass Ihre neue Eigenschaft auf der Benutzeroberfläche nicht vorhanden ist. Wenn Sie eine der erforderlichen Eigenschaften löschen, wird sich TypeScript ebenfalls beschweren. Dies stellt sicher, dass Sie immer die richtigen Daten haben und Objekte immer genau wie erwartet aussehen.

Nachdem wir die Daten haben, können wir überprüfen, ob der Benutzer die richtige Stadtanfrage gesendet hat.

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

Wenn diese Aussage wahr ist, hat der Benutzer etwas falsch gemacht, daher müssen wir eine 400-Antwort zurückgeben.

Wir könnten den Code hier einfach manuell eingeben, aber wir werden ein neues apiResponsesObjekt mit Methoden für einige der möglichen API-Antwortcodes erstellen .

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), }; }, };

Dies erleichtert die spätere Wiederverwendung in der Datei erheblich. Sie sollten auch sehen, dass wir eine Eigenschaft von haben body: { [key: string]: any }. Dies besagt, dass diese Funktion eine Eigenschaft des Körpers hat, die ein Objekt sein muss. Dieses Objekt kann Schlüssel haben, die einen Wert eines beliebigen Typs haben.

Da wir wissen, dass dies bodyimmer eine Zeichenfolge sein wird, können JSON.stringifywir sicherstellen, dass wir einen Zeichenfolgenkörper zurückgeben.

Wenn wir diese Funktion zu unserem Handler hinzufügen, erhalten wir Folgendes:

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]); };

Wenn der Benutzer eine Stadt oder eine Stadt, für die wir keine Daten haben, nicht übergeben hat, geben wir eine 400 mit einer Fehlermeldung zurück. Wenn die Daten vorhanden sind, geben wir eine 200 mit einem Datenkörper zurück.

Hinzufügen einer neuen Übersetzungs-API

Im vorherigen Abschnitt haben wir unser TypeScript-API-Repo eingerichtet und ein Lambda erstellt, das nur fest codierte Daten verwendet.

This part is going to teach you how to use the aws-sdk to interact directly with other AWS services to create a really powerful API.

To start, we need to add a new file for our translation API. Create a new file under the lambdas folder called translate.ts. We can start this file out with some basic boilerplate code. This is the starting code for a TypeScript API Lambda.

import { APIGatewayProxyHandler } from 'aws-lambda'; import 'source-map-support/register'; export const handler: APIGatewayProxyHandler = async (event) => { };

Now we need to get the text that the user wants translated and the language that they want to translate to. We can get these from the body of the request.

One extra thing we have to do here is to parse the body. By default, API Gateway stringifies any JSON passed in the body. We can then destructure the text and language from the body.

const body = JSON.parse(event.body); const { text, language } = body;

We now need to check that the user has passed up text and language.

if (!text) { // retrun 400 } if (!language) { // return 400 }

In the last part we created the 400 response as a function in the file. As we're going to be using these API responses across multiple files, it is a good idea to pull them out to their own common file.

Create a new folder under lambdas called common. This is where we are going to store all common functions.

In that folder create a new file called apiResponses.ts. This file is going to export the apiResponses object with the _200 and _400 methods on it. If you have to return other response codes then you can add them to this object.

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), }; }, }; export default apiResponses;

We can now import that object into our code and use these common methods in our code. At the top of our translate.ts file we can now add this line:

import apiResponses from './common/apiResponses';

and update our text and language checks to call the _400 method on that object:

if (!text) { return apiResponses._400({ message: 'missing text fom the body' }); } if (!language) { return apiResponses._400({ message: 'missing language from the body' }); }

With that completed we know that we have the text to translate and a language to translate into, so we can start the translation process.

Using the aws-sdk is almost always an async task so we're going to wrap it in a try/catch so that our error handling is easier.

try { } catch (error) { }

The first thing we need to do is to import the aws-sdk and create a new instance of the translate service.

To do that we need to install the aws-sdk and then import it. First run npm install --save aws-sdk and then add this code to the top of your translate file:

import * as AWS from 'aws-sdk'; const translate = new AWS.Translate();

With this we can start to write our translation code. We're going to start with the line that does the translation first. Add this in the try section.

const translatedMessage = await translate.translateText(translateParams).promise();

One thing that some of you may have noticed is that we're passing in translateParams without having defined it yet. That is because we're not sure what type it is yet.

To find this out we can use a tool in VS Code called go to definition. This allows us to jump to where the function if defined so we can find out what the type of the parameters is. You can either right click and select go to definition or hold Ctrl and click on the function.

As you can see the translateText function takes a param of Translate.Types.TranslateTextRequest.

Another way to find this out is to use intelisense by mousing over the translateText function. You should see this, where you can see that params: AWS.Translate.TranslateTextRequest:

With this we can create our translate params above the translate request we made earlier. We can then populate it based on the type we are setting it as. This makes sure we're passing up the correct fields.

const translateParams: AWS.Translate.Types.TranslateTextRequest = { Text: text, SourceLanguageCode: 'en', TargetLanguageCode: language, };

Now that we have the parameters and are passing them into the translate.translateText function, we can start creating our response. This is just going to be a 200 response with the translated message.

return apiResponses._200({ translatedMessage });

With that all done we can move onto the catch section. In here we just want to log out the error and then return a 400 response from the common file.

console.log('error in the translation', error); return apiResponses._400({ message: 'unable to translate the message' });

With that completed we're done with our lambda code, so need to move into our severless.ts file to add this new API endpoint and give it the permissions it needs.

In the serverless.ts file we can scroll down to the functions section. In here we need to add a new function to the object.

translate: { handler: 'lambdas/translate.handler', events: [ { http: { path: 'translate', method: 'POST', cors: true, }, }, ], },

The main difference between this and the previous endpoint is that the endpoint is now a POST method. This means if you try and do a GET request to this URL path, you'll get an error response.

The last thing to do is to give the lambdas permission to use the Translate service. With almost all of the AWS Services, you'll need to add extra permissions to be able to use the from within a lambda.

To do this we add a new field onto the provider section called iamRoleStatements. This is an array of allow or deny statements for different services and resources.

iamRoleStatements: [ { Effect: 'Allow', Action: ['translate:*'], Resource: '*', }, ],

With this added in we have everything we need set up so we can run sls deploy to deploy our new API.

Once this has deployed, we can get the API URL and use a tool like postman or postwoman.io to make a request to that URL. We just need to pass up a body of:

{ "text": "This is a test message for translation", "language": "fr" }

and then we should get a 200 response of:

{ "translatedMessage": { "TranslatedText": "Ceci est un message de test pour la traduction", "SourceLanguageCode": "en", "TargetLanguageCode": "fr" } }

Summary

In this article we've learnt how to:

  • Set up a new TypeScript repo with severless create --template aws-nodejs-typescript
  • Add our own Lambda that returns a selection of hardcoded data
  • Added that Lambda as an API endpoint
  • Added another Lambda which will automatically translate any text passed to it
  • Added an API endpoint and gave the Lambda the permissions it needed to work

If you enjoyed this article and want to learn more about Serverless and AWS, then I have a Youtube Channel with over 50 videos on all of this. I'd recommend watching the videos you find most interesting in my Serverless and AWS playlist.