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 getting, setting and editing basic user profile information using a Nitric key value store 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:
Method | Route | Description |
---|---|---|
GET | /profiles/[id] | Get a specific profile by its Id |
POST | /profiles | Create a new profile |
DELETE | /profiles/[id] | Delete 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:
Method | Route | Description |
---|---|---|
GET | /profiles/[id]/image/upload | Get a profile image upload URL |
GET | profiles/[id]/image/download | Get a profile image download URL |
GET | profiles/[id]/image/view | View the image that is downloaded |
Prerequisites
- Node.js
- The Nitric CLI
- (optional) Your choice of an AWS, GCP or Azure account
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 ts-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:
services/
├── hello.ts
node_modules/
nitric.yaml
package.json
README.md
You can test the project to verify everything is working as expected:
nitric start
If everything's working you can now delete all files in the services/
folder, we'll create new services 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 services directory and add the following code to that file.
import { api, kv } from '@nitric/sdk'
import { v4 as uuid } from 'uuid'
// Create an API named 'public'
const profileApi = api('public')
// Define a key value store named 'profiles', then request getting and setting permissions.
const profiles = kv('profiles').for('getting', 'setting')
Here we're creating an API named public
and key value store named profiles
, then requesting get and set permissions which allows our function to access the key value store.
Resources in Nitric like api
and key value store
represent high-level
cloud resources. When your app is deployed Nitric automatically converts these
requests into appropriate resources for the specific
provider. Nitric also takes care of adding the IAM
roles, policies, etc. that grant the requested access. For example the key value store
resource uses DynamoDB in AWS or FireStore on Google Cloud.
We will need some helper functions to serialize and deserialize our profiles for the keyvalue store.
// Helper function to get current profiles
async function getProfiles() {
try {
const serializedList = await profiles.get('profiles')
return serializedList && serializedList['ids']
? JSON.parse(serializedList['ids'])
: []
} catch (error) {
await profiles.set('profiles', { ids: [] })
return []
}
}
// Helper function to update profiles list
async function updateProfiles(profileList) {
try {
const updatedSerializedList = JSON.stringify(profileList)
await profiles.set('profiles', { ids: updatedSerializedList })
} catch (error) {
console.error('Error updating profiles:', error)
}
}
Create profiles with POST
Next we will add features that allow our API consumers to work with profile data.
You could separate some or all of these handlers into their own files if you prefer. For simplicity we'll group them together in this guide.
profileApi.post('/profiles', async (ctx) => {
let id = uuid()
const { name, age, homeTown } = ctx.req.json()
const profileList = await getProfiles()
profileList.push({
id,
name,
age,
homeTown,
})
await updateProfiles(profileList)
// 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 {
const profileList = await getProfiles()
const profile = profileList.find((profile) => profile.id === id)
if (profile != undefined) {
ctx.res.json(profile)
} else {
throw new Error()
}
} catch (error) {
ctx.res.status = 404
ctx.res.json({
msg: `Profile with id ${id} not found.`,
})
}
})
Remove a profile with DELETE
profileApi.delete('/profiles/:id', async (ctx) => {
const { id } = ctx.req.params
try {
const profileList = await getProfiles()
await updateProfiles(profileList.filter((profile) => profile.id !== id))
} 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.
nitric start
Once it starts, your services 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.
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]'
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.
nitric stack new dev
AWS
Cloud deployments incur costs and while most of these resource are available with free tier pricing you should consider the costs of the deployment.
We can deploy our application using the following command:
nitric up
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 re-running 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'