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!

Video

You can check also check out the video version.

The source code of this tutorial is on GitHub

What we’ll do

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. 🎉

Assumptions

Before getting started, ensure you have Node.js and Docker installed.

Getting Started

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.

Creating our Function Handlers

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

Type safety

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.

Creating Customers

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();

List Customers

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();

Reading an individual customer

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();

Defining our API

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

Putting everything together

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

Run and test your Stack

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?

Deployment

Now that your customers API is ready, you can deploy your app to the cloud provider of your choice.

Next Steps

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