Building a REST API with Nitric

This guide will show you how to build a serverless REST API with the Nitric framework using Node.js. The example API enables reading, writing and editing basic user profile information using a Nitric collection to store user data. Once the API is created we'll test it locally, then optionally deploy it to a cloud of your choice.

The API will provide the following routes:

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

There is also an extended section of the guide that adds file operations using a Nitric bucket to store and retrieve profile pictures. The extension adds these routes to the API:

MethodRouteDescription
GET/profiles/[id]/image/uploadGet a profile image upload URL
GETprofiles/[id]/image/downloadGet a profile image download URL
GETprofiles/[id]/image/viewView the image that is downloaded

Video

Here's a video of this guide built with Node.js - Build and Deploy a REST API for any Cloud.

Prerequisites

Getting started

Let's start by creating a new project from a Nitric TypeScript template, this will provide a base to start building the API.

nitric new my-profile-api "official/TypeScript - Starter"

Next, open the project in your editor of choice.

cd my-profile-api

Make sure all dependencies are resolved:

Using NPM:

npm 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 to verify everything is working as expected:

npm run dev

If everything's working you can now delete all files in the functions/ folder, we'll create new functions in this guide.

Building the API

This example uses UUIDs to create unique IDs to store profiles against, let's start by adding a library to help with that:

npm install uuid

Applications built with Nitric can contain many APIs, let's start by adding one to this project to serve as the public endpoint. Create a file named profiles.ts in the functions directory and add the following code to that file.

functions/profiles.ts
import { api, collection } from '@nitric/sdk'
import { v4 as uuid } from 'uuid'

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

// Define a collection named 'profiles', then request reading and writing permissions.
const profiles = collection('profiles').for('writing', 'reading')

Here we're creating an API named public and collection named profiles, then requesting read and write permissions which allows our function to access the collection.

Create profiles with POST

Let's start adding features that allow our API consumers to work with profile data.

profileApi.post('/profiles', async (ctx) => {
  let id = uuid()

  // Store the new profile in the profiles collection
  await profiles.doc(id).set({
    name: ctx.req.json().name,
    age: ctx.req.json().age,
    homeTown: ctx.req.json().homeTown,
  })

  // Set a JSON HTTP response
  ctx.res.json({
    msg: `Profile with id $[id] created.`,
  })
})

Retrieve a profile with GET

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

  try {
    // Retrieve and return the profile data
    const profile = await profiles.doc(id).get()
    return ctx.res.json(profile)
  } catch (error) {
    // log errors and return a Not Found status.
    console.error(error)
    ctx.res.status = 404
    ctx.res.json({
      msg: `Profile with id $[id] not found.`,
    })
  }
})

List all profiles with GET

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

Remove a profile with DELETE

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.`,
    })
  }
})

Ok, let's run this thing!

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

npm run dev

Once it starts, the application will receive requests via the API port. You can use the Local Dashboard or any HTTP client to test the API. We'll keep it running for our tests. If you want to update your functions, just save them, they'll be reloaded automatically.

Test the API

Below are some example requests you can use to test the API. You'll need to update all values in brackets [] and change the URL to your deployed URL if you're testing on the cloud.

Create Profile

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

Fetch Profile

curl --location --request GET 'http://localhost:4001/profiles/[id]'

Fetch All Profiles

curl --location --request GET 'http://localhost:4001/profiles'

Delete Profile

curl --location --request DELETE 'http://localhost:4001/profiles/[id]'

Deploy to the cloud

At this point, you can deploy the application to any supported cloud provider. Start by setting up your credentials and any configuration for the cloud you prefer:

Next, we'll need to create a stack. Stacks represent deployed instances of an application, including the target provider and other details such as the deployment region. You'll usually define separate stacks for each environment such as development, testing and production. For now, let's start by creating a dev stack.

nitric stack new
? What should we name this stack? dev
? Which provider do you want to deploy with? aws
? Which region should the stack deploy to? us-east-1

AWS

In the previous step we called our stack dev, let's try deploying it with the up command.

nitric up
┌───────────────────────────────────────────────────────────────┐
| 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. If you'd like to make changes to the API you can apply those changes by rerunning the up command. Nitric will automatically detect what's changed and just update the relevant cloud resources.

When you're done testing your application you can tear it down from the cloud, use the down command:

nitric down

Optional - Add profile image upload/download support

If you want to go a bit deeper and create some other resources with Nitric, why not add images to your profiles API.

Access profile buckets with permissions

Define a bucket named profilesImg with reading/writing permissions.

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

Get a URL to upload a profile image

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

  // Return a signed upload URL, which provides temporary access to upload a file.
  const photoUrl = await profilesImg
    .file(`images/$[id]/photo.png`)
    .getUploadUrl()
  ctx.res.json({
    url: photoUrl,
  })
})

Get a URL to download a profile image

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

  // Return a signed download URL, which provides temporary access to download a file.
  const photoUrl = await profilesImg
    .file(`images/$[id]/photo.png`)
    .getDownloadUrl()
  ctx.res.json({
    url: photoUrl,
  })
})

You can also return a redirect response that takes the HTTP client directly to the photo URL.

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

  // Redirect to a signed read-only file URL.
  const photoUrl = await profilesImg
    .file(`images/$[id]/photo.png`)
    .getDownloadUrl()
  ctx.res.status = 303
  ctx.res.headers['Location'] = [photoUrl]
})

Test the extended API

Update all values in brackets [] and change the URL to your deployed URL if you're testing on the cloud.

Get an image upload URL

curl --location --request GET 'http://localhost:4001/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 an image download URL

curl --location --request GET 'http://localhost:4001/profiles/[id]/image/download'