20
Build a Multi-Cloud API in 10 minutes
Working on a REST API you want to deploy to the cloud, but aren’t sure cloud providers, deployment, scaling, persistence tech, etc.? There's also the big decision of which cloud provider to deploy to AWS, Google Cloud, Microsoft Azure? I can feel the decision fatigue headache already!
Let's get started!
You can check also check out the video version.
The source code of this tutorial is on GitHub
We'll build a simple API that can create and list customers. To help get started we'll use Nitric’s Typescript Stack template as a foundation, this is available via the Nitric CLI.
Next, we'll change the stack by creating new functions to handle our customer API requests. To store and access our customers we'll use a document database via the Nitric SDK for Node.
Finally, we'll define our API using OAS 3.0, run it locally for testing, then you're ready to deploy. 🎉
First, let’s install the Nitric CLI using npm:
npm install -g @nitric/cli
You can create your Stack by running the make:stack
command below:
nitric make:stack
Follow the prompts and select the Typescript template.
Next, open the project in your editor of choice, you should see the following structure:
rest-api
├── common
│ ├── example.ts
│ ├── index.ts
│ └── path.ts
├── functions
│ ├── create.ts
│ ├── list.ts
│ └── read.ts
├── .gitignore
├── api.yaml
├── nitric.yaml
├── package.json
└── yarn.lock
The stack template already comes with a working API example, so we'll use this as a starter for our customers API.
To create and list our customers we'll use the documents API from the Nitric SDK. This API has a cloud agnostic syntax, this means it will automatically use Firestore on GCP, DynamoDB on AWS or Cosmos on Azure. Learn once, write once, deploy anywhere.
Time to install our dependencies, from your project directory run:
yarn install
We are using Typescript, so let’s create a Customer interface we can use in our functions to ensure type safety. In the common
folder, create a new file called customer.ts
with the following content:
// common/customer.ts
export interface Customer {
name: string;
email: string;
}
The interface helps with type-safety, we can then enforce it with our API definition.
Let's turn the create function into the handler for POST: /customers
, having it add new customers to a document collection called customers
. Since new customers will need a unique id, for the purposes of this tutorial we'll generate a uuid to serve as the id (uuidv4 is included as a dependency).
// functions/create.ts
import { faas, documents } from "@nitric/sdk";
import { Customer } from "../common";
import { uuid } from "uuidv4";
interface CreateContext extends faas.HttpContext {
req: faas.HttpRequest & {
body?: Customer;
};
}
// Start your function here
faas
.http(
faas.json(), // use json body parser middleware to decode data
async (ctx: CreateContext): Promise<faas.HttpContext> => {
const customer = ctx.req.body;
// generate a new uuid
const id = uuid();
// Create a new customer document
await documents().collection("customers").doc(id).set(customer);
ctx.res.body = new TextEncoder().encode(
`Created customer with ID: ${id}`
);
return ctx;
}
)
.start();
Next, let's update the list function for GET: /customers, which will retrieve all customers:
// functions/list.ts
import { faas, documents } from "@nitric/sdk";
import { Customer } from "../common";
// Start your function here
faas
.http(async (ctx: faas.HttpContext): Promise<faas.HttpContext> => {
try {
// retrieves all customers from the customers collection
const customers = await documents()
.collection<Customer>("customers")
.query()
.fetch();
const customerResults = [];
for (const customer of customers.documents) {
customerResults.push(customer.content);
}
ctx.res.json(customerResults);
} catch (e) {
ctx.res.status = 500;
ctx.res.body = new TextEncoder().encode("An unexpected error occurred");
}
return ctx;
})
.start();
The final function will read a customer using their id. To retrieve the id from the request path we will use a helper function called path
which is located in the common/path.ts
file. Let's update this function to retrieve the id from the customers path:
// common/path.ts
import { Path } from "path-parser";
export const path = new Path("/customers/:id");
Note that we are utilizing an awesome library called path-parser to extract the id from the path, there's no point reinventing the wheel.
Now update the read function to retrieve a customer.
// functions/read.ts
import { faas, documents } from "@nitric/sdk";
import { Customer, path } from "../common";
// Start your function here
faas
.http(async (ctx: faas.HttpContext): Promise<faas.HttpContext> => {
// get params from path
const { id } = path.test(ctx.req.path);
if (!id) {
ctx.res.body = new TextEncoder().encode("Invalid Request");
ctx.res.status = 400;
}
try {
console.log("getting doc id", id);
const customer = await documents()
.collection<Customer>("customers")
.doc(id)
.get();
ctx.res.json(customer);
} catch (e) {
ctx.res.status = 500;
ctx.res.body = new TextEncoder().encode("An unexpected error occurred");
}
return ctx;
})
.start();
The Nitric framework takes full advantage of the OpenAPI Specification to define and deploy your APIs. APIs are typically defined in an api.yaml
file, so let’s start defining:
openapi: 3.0.0
info:
version: 1.0.0
title: Customer API
description: Customer API
paths:
/customers:
get:
operationId: customers-list
x-nitric-target:
name: list
type: function
description: Retrieve all customers
responses:
"200":
description: Successful response
post:
operationId: customers-create
x-nitric-target:
name: create
type: function
description: Creates and persists new customers
responses:
"200":
description: Successful response
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CustomersCreate"
/customers/{customerId}:
get:
operationId: customers-read
parameters:
- in: path
name: customerId
schema:
type: string
required: true
x-nitric-target:
name: read
type: function
description: Retrieve an existing customer by its ID
responses:
"200":
description: Successful response
components:
schemas:
CustomersCreate:
type: object
properties:
name:
type: string
email:
type: string
Let’s review our nitric stack before we start running it locally. Change the examples collection and the api reference to customers:
name: customers
# Nitric functions
functions:
create:
handler: functions/create.ts
read:
handler: functions/read.ts
list:
handler: functions/list.ts
# Nitric collections
collections:
customers: {}
# Nitric APIs
apis:
customers: api.yaml
To run our stack locally, use the nitric run
command. This will build and run your app locally using containers, exposing the API Gateway. Once complete, you should see an output like this:
✔ Building Services
✔ Creating docker network
✔ Running Storage Service
✔ Starting API Gateways
✔ Starting Entrypoints
Function Port
──────── ─────
create 54002
list 54003
read 54004
Api Port
─────── ─────
customers 49152
Let’s test our API with cURL (to see testing using a postman like experience, view the video version), create customers by calling the API with different body data:
# POST: /customers
curl -H "Content-Type: application/json" -X POST -d '{"name":"David","email": "[email protected]"}' http://localhost:49152/customers
You will see a successful output containing the generated customer id.
Now you can retrieve this customer from the API:
# GET: /customers/{customerId}
curl http://localhost:49152/customers/YOUR_CUSTOMER_ID
Try adding a few more customers and then list all of them:
# GET: /customers
curl http://localhost:49152/customers
Yay, see how easy that was?
Now that your customers API is ready, you can deploy your app to the cloud provider of your choice.
Prefer Go, Python or Java? Why not try using a different language via our other Stack templates and SDKs.
Want to learn more? Visit our latest guides and documentation. if you have any questions, check out our GitHub discussions page 😃
20