Build your first API for the cloud with Nitric

Build your first API for the cloud with Nitric

Run locally or Deploy to AWS, GCP, or Azure

·

7 min read

Originally posted on the Nitric site.

Ready to get started building and deploying applications to the cloud? Let's get started with a simple API to learn the basics, and get you started on your journey.

What we'll do

  1. Use Nitric to create an API to create and update profiles
  2. Create handlers for the following API operations
  3. Running locally for testing
  4. Deploy to a cloud of your choice
  5. (Optional) Add handlers for the following API operations
MethodRouteDescription
GET/profiles/{id}Get a specific profile by its Id
GET/profilesList all profiles
POST/profilesCreate a new profile
DELETE/profiles/{id}Delete a profile
PUT/profiles/{id}Update a profile
GET/profiles/{id}/image/uploadGet a profile image upload URL
GETprofiles/{id}/image/downloadGet a profile image download URL

Getting started

This tutorial assumes you have the Nitric CLI installed. If not, follow this quick installation guide.

We’ll start by creating a new project for our API.

nitric new

Create a project name, select the TypeScript template and choose the default glob handler.

? What is the name of the stack? my-profile-api
? Choose a template: official/TypeScript - Starter
? Glob for the function handlers? functions/*.ts

Next, open the project in your editor of choice.

> cd my-profile-api

Make sure all dependencies are resolved with yarn or npm.

yarn install

The scaffolded project should have the following structure:

+--functions/
|  +-- hello.ts
+--node_modules/
|  ...
+--nitric.yaml
+--package.json
+--README.md

You can test the project scaffold with the run command.

nitric run

The first time you run a project or function it will take a moment longer to start while Docker image layers are downloaded and cached on your machine.

SUCCESS  Configuration gathered (3s)
SUCCESS  Created Dev Image! (1s)
SUCCESS  Started Local Services! (2s)
SUCCESS  Started Functions!(0s)

Local running, use ctrl-C to stop

Api  | Endpoint
main | http://localhost:9001/apis/main

The template project is successfully scaffolded.

Since we won't use the example function you can now delete the functions/hello.ts file.

Add uuidv4 to your project

We'll need a unique identifier to store our profiles against.

> yarn add uuidv4

Coding our Profile API

Let's start by initializing our profiles api, create a file named 'profiles.ts' within functions and add the following code.

import { api, collection } from '@nitric/sdk';
import { uuid } from 'uuidv4';

// Create an api named public
const profileApi = api('public');

// Access profile collection with permissions
const profiles = collection('profiles').for('writing', 'reading');

Here we are defining the following -

  • an API named public,
  • a collection named profiles with reading/writing permissions

Create profile with a post method

profileApi.post('/profiles', async (ctx) => {
  let id = uuid();
  let name = ctx.req.json().name;
  let age = ctx.req.json().age;
  let homeTown = ctx.req.json().homeTown;

  // Create the new profile
  await profiles.doc(id).set({ name, age, homeTown });

  // Return the id
  ctx.res.json({
    msg: `Profile with id ${id} created.`,
  });
});

Retrieve profile with a get method

profileApi.get('/profiles/:id', async (ctx) => {
  const { id } = ctx.req.params;

  // Return the profile
  try {
    const profile = await profiles.doc(id).get();
    return ctx.res.json(profile);
  } catch (error) {
    ctx.res.status = 404;
    ctx.res.json({
      msg: `Profile with id ${id} not found.`,
    });
  }
});

Retrieve all profiles with a get method

profileApi.get('/profiles', async (ctx) => {
  // Return all profiles
  ctx.res.json({
    output: await profiles.query().fetch(),
  });
});

Remove profile with a delete method

profileApi.delete('/profiles/:id', async (ctx) => {
  const { id } = ctx.req.params;

  // Delete the profile
  try {
    profiles.doc(id).delete();
  } catch (error) {
    ctx.res.status = 404;
    ctx.res.json({
      msg: `Profile with id ${id} not found.`,
    });
  }
});

Update profile with a put method

profileApi.put('/profiles/:id', async (ctx) => {
  const { id } = ctx.req.params;
  const doc = profiles.doc(id);

  try {
    // Update values provided
    const current = await doc.get();
    let name = ctx.req.json().name ?? current['name'] ?? '';
    let age = ctx.req.json().age ?? current['age'] ?? '';
    let homeTown = ctx.req.json().homeTown ?? current['homeTown'] ?? '';

    // Create or Update the profile
    await doc.set({ name, age, homeTown });
  } catch (error) {
    ctx.res.status = 404;
    ctx.res.json({
      msg: `Profile with id ${id} not found.`,
    });
  }
});

Nitric will automatically infer the required specification and permissions to create an API Gateway - Learn more.

Run it!

Now that you have an API defined with handlers for each of its methods, it's time to test it out locally.

Test out your application with the run command:

nitric run

Note: run starts a container to act as an API gateway, as well as a container for each of the services.

 SUCCESS Configuration gathered (3s)
 SUCCESS  Created Dev Image! (2s)
 SUCCESS  Started Local Services! (2s)
 SUCCESS  Started Functions! (1s)
Local running, use ctrl-C to stop

Api    | Endpoint
public | http://localhost:9001/apis/public

Once it starts, the application will receive requests via the API port. You can use cURL, Postman or any other HTTP client to test the API.

Pressing 'Ctrl-C' will end the application.

Test your API

Update all {} values and change the URL to your deployed URL if you're testing on the cloud.

Create Profile

curl --location --request POST 'http://127.0.0.1:9001/apis/public/profiles' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "Peter Parker",
    "age": "21",
    "homeTown" : "Queens"
}'

Fetch Profile

curl --location --request GET 'http://127.0.0.1:9001/apis/public/profiles/{id}'

Fetch All Profiles

curl --location --request GET 'http://127.0.0.1:9001/apis/public/profiles'

Update Profile

curl --location --request PUT 'http://127.0.0.1:9001/apis/public/profiles/{id}' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "Ben Reily",
    "homeTown" : "Las Vegas"
}'

Delete Profile

curl --location --request DELETE 'http://127.0.0.1:9001/apis/public/profiles/{id}'

Deploy to the cloud

Setup your credentials and any other cloud specific configuration:

Create a stack - a collection of resources identified in your project which will be deployed.

nitric stack new
? What do you want to call your new stack? dev
? Which Cloud do you wish to deploy to? aws
? select the region us-east-1

AWS

Note: You are responsible for staying within the limits of the free tier or any costs associated with deployment.

We called our stack dev, lets try deploying it with the up command

nitric up -s dev
┌───────────────────────────────────────────────────────────────┐
| API  | Endpoint                                               |
| main | https://XXXXXXXX.execute-api.us-east-1.amazonaws.com   |
└───────────────────────────────────────────────────────────────┘

When the deployment is complete, go to the relevant cloud console and you'll be able to see and interact with your API.

To undeploy run the following command.

nitric down -s dev

Optional - Add profile image upload/download support

Access profile buckets with permissions

Define a bucket named profilesImg with reading/writing permissions

const profilesImg = bucket('profilesImg').for('reading', 'writing');

Get Signed URL to Upload Profile Image

profileApi.get('/profiles/:id/image/upload', async (ctx) => {
  const id = ctx.req.params['id'];

  // Return a signed url reference for upload
  const photo = profilesImg.file(`images/${id}/photo.png`);
  const photoUrl = await photo.signUrl(FileMode.Write);

  ctx.res.json({
    url: photoUrl,
  });
});

Get Signed URL to Download Profile Image

profileApi.get('/profiles/:id/image/download', async (ctx) => {
  const id = ctx.req.params['id'];
  const photo = profilesImg.file(`images/${id}/photo.png`);

  // Return a signed url reference for download
  const photoUrl = await photo.signUrl(FileMode.Read);
  ctx.res.json({
    url: photoUrl,
  });
});

You can also directly redirect to the photo url.

profileApi.get('/profiles/:id/image/view', async (ctx) => {
  const { id } = ctx.req.params;
  const photo = profilesImg.file(`images/${id}/photo.png`);

  // Create a read-only signed url reference
  const photoUrl = await photo.signUrl(FileMode.Read);
  ctx.res.status = 303;
  ctx.res.headers['Location'] = [photoUrl];
});

Test your API

Update all {} values and change the URL to your deployed URL if you're testing on the cloud.

Get upload image URL

curl --location --request GET 'http://127.0.0.1:9001/apis/public/profiles/{id}/image/upload'

Using the upload URL with curl

curl --location --request PUT '{url}' \
--header 'content-type: image/png' \
--data-binary '@/home/user/Pictures/photo.png'

Get download image URL

curl --location --request GET 'http://127.0.0.1:9001/apis/public/profiles/{id}/image/download'

Summary

That's it! You've got your first API deployed to the cloud. What's next?

We've got other guides on the Nitric website, and a lot of other great features to check out in our docs.