Guides
Building your first API with Nitric
What we'll be doing
- Use Nitric to create an API to create and update profiles
- Create handlers for the following API operations
Method | Route | Description |
---|---|---|
GET | /profiles/{id} | Get a specific profile by its Id |
GET | /profiles | List all profiles |
POST | /profiles | Create a new profile |
DELETE | /profiles/{id} | Delete a profile |
PUT | /profiles/{id} | Update a profile |
- Run locally for testing
- Deploy to a cloud of your choice
- (Optional) Add handlers for the following API operations
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 |
Video
Here's a video of this guide built with Node.js:
Build and Deploy a REST API for any Cloud
Prerequisites
- Node.js
- The Nitric CLI
- An AWS, GCP or Azure account (your choice)
- Pipenv - for simplified dependency management
- The Nitric CLI
- An AWS, GCP or Azure account (your choice)
Getting started
We’ll start by creating a new project for our API.
nitric new
Create a project, name it and select your preferred starter template.
? What is the name of the project? my-profile-api
? Choose a template: official/TypeScript - Starter
? What is the name of the project? my-profile-api
? Choose a template: official/Python - Starter
Next, open the project in your editor of choice.
> cd my-profile-api
Make sure all dependencies are resolved:
Using NPM:
npm install
Using Pipenv:
pipenv install --dev
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
Note: the
dev
script starts the Nitric Server usingnitric start
, which provides local interfaces to emulate cloud resources, then runs your functions and allows them to connect.
+--functions/
| +-- hello.py
+--nitric.yaml
+--Pipfile
+--Pipfile.lock
+--README.md
You can test the project to verify everything is working as expected:
Start the Nitric server to emulate cloud services on your machine:
nitric start
Next, in a new terminal window, you can run your application:
pipenv run dev
If everything is working as expected you can now delete all files in the functions/
folder, we'll create new functions in this guide.
Building the Profile API
In this example we'll use UUIDs to create unique IDs to store profiles against, let's start by adding a library to help with that:
npm install uuidv4
Next, let's start building the profiles API. Create a file named 'profiles.ts' in the functions directory and add the following:
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');
Let's start building our profiles API. Create a file named 'profiles.py' in the functions directory and add the following:
from uuid import uuid4
from nitric.resources import api, collection, bucket
from nitric.application import Nitric
# Create an api named public
profile_api = api("public")
# Access profile collection with permissions
profiles = collection('profiles').allow('reading','writing')
Here we're creating:
- An API named
public
, - A collection named
profiles
and giving our function permission to read and write to that collection.
From here, let's add some features to that function that allow us to work with profiles.
Note: You could separate some or all of these request handlers their own functions if you prefer. For simplicity we'll group them together in this guide.
Create profiles with POST
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.`,
});
});
@profile_api.post("/profiles")
async def create_profile(ctx):
pid = str(uuid4())
name = ctx.req.json['name']
age = ctx.req.json['age']
hometown = ctx.req.json['homeTown']
await profiles.doc(pid).set({ 'name': name, 'age': age, 'hometown': hometown} )
ctx.res.body = { 'msg': f'Profile with id {pid} created.'}
Retrieve a profile with GET
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.`,
});
}
});
@profile_api.get("/profiles/:id")
async def get_profile(ctx):
pid = ctx.req.params['id']
d = await profiles.doc(pid).get()
ctx.res.body = f"{d.content}"
List all profiles with GET
profileApi.get('/profiles', async (ctx) => {
// Return all profiles
ctx.res.json({
output: await profiles.query().fetch(),
});
});
@profile_api.get("/profiles")
async def get_profiles(ctx):
results = await profiles.query().fetch()
r = [doc.content for doc in results.documents]
ctx.res.body = f"{r}"
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.`,
});
}
});
@profile_api.delete("/profiles/:id")
async def delete_profiles(ctx):
pid = ctx.req.params['id']
try:
d = await profiles.doc(pid).delete()
ctx.res.body = { 'msg': f'Profile with id {pid} deleted.'}
except:
ctx.res.status = 404
ctx.res.body = { 'msg': f'Profile with id {pid} not found.'}
Nitric.run()
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
Note: the
dev
script starts the Nitric Server usingnitric start
, which provides local interfaces to emulate cloud resources, then runs your functions and allows them to connect.
Start the Nitric server to emulate cloud services on your machine:
nitric start
Next, in a new terminal window, you can run your application:
pipenv run dev
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.
We will keep it running for our tests. If you want to update your functions, just save them, they'll be reloaded automatically.
Test your API
Update all values in {} 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 what you've built to any of the supported cloud providers. To do this start by setting up your credentials and any configuration for the cloud you prefer:
Next, we'll need to create a stack
. A stack represents a deployed instance of an application, which is a collection of resources defined in your project. You might want separate stacks for each environment, such as stacks for dev
, test
and prod
. For now, let's start by creating a dev
stack.
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
, 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.
To tear down your application 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');
photos = bucket("photos").allow('reading','writing')
Add imports for time and date so that we can set up caching/expiry headers
from datetime import datetime, timedelta
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 url reference for upload
const photoUrl = await profilesImg
.file(`images/${id}/photo.png`)
.getUploadUrl();
ctx.res.json({
url: photoUrl,
});
});
@profile_api.get("/profiles/:id/image/upload")
async def upload_profile_image(ctx):
pid = ctx.req.params['id']
photo = photos.file(f'images/{pid}/photo.png')
photo_url = await photo.upload_url(expiry=3600)
expires = datetime.utcnow() + timedelta(seconds=(3600))
expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
ctx.res.headers['Expires'] = expires
ctx.res.body = photo_url
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 url reference for download
const photoUrl = await profilesImg
.file(`images/${id}/photo.png`)
.getDownloadUrl();
ctx.res.json({
url: photoUrl,
});
});
@profile_api.get("/profiles/:id/image/view")
async def download_profile_image(ctx):
pid = ctx.req.params['id']
photo = photos.file(f'images/{pid}/photo.png')
photo_url = await photo.download_url(expiry=3600)
expires = datetime.utcnow() + timedelta(seconds=(3600))
expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
ctx.res.headers['Expires'] = expires
ctx.res.body = photo_url
You can also directly redirect to the photo URL.
profileApi.get('/profiles/:id/image/view', async (ctx) => {
const { id } = ctx.req.params;
// Create a read-only signed url reference
const photoUrl = await profilesImg
.file(`images/${id}/photo.png`)
.getDownloadUrl();
ctx.res.status = 303;
ctx.res.headers['Location'] = [photoUrl];
});
@profile_api.get("/profiles/:id/image/view")
async def download_profile_image(ctx):
pid = ctx.req.params['id']
photo = photos.file(f'images/{pid}/photo.png')
photo_url = await photo.download_url(expiry=3600)
expires = datetime.utcnow() + timedelta(seconds=(3600))
expires = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
ctx.res.headers['Expires'] = expires
ctx.res.headers['Location'] = [photo_url]
ctx.res.status = 303
Time to test the updated API
Update all values in {} 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'