DEV Community

Rak
Rak

Posted on

Building your first API with Nitric in Python

Nitric is a serverless framework for rapidly developing and deploying cloud applications.

This guide shows you how use Nitric to create an API to create and update profiles. We'll also make use collections for data persistence. It was originally posted here on the Nitric site.

What we'll be doing

  1. Use Nitric to create an API to create and update profiles
  2. 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
  1. Run locally for testing
  2. Deploy to a cloud of your choice
  3. (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

Prerequisites

Getting started

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

nitric new
Enter fullscreen mode Exit fullscreen mode

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/Python - Starter
Enter fullscreen mode Exit fullscreen mode

Next, open the project in your editor of choice.

> cd my-profile-api
Enter fullscreen mode Exit fullscreen mode

Make sure all dependencies are resolved using Pipenv:

pipenv install --dev
Enter fullscreen mode Exit fullscreen mode

The scaffolded project should have the following structure:

+--functions/
|  +-- hello.py
+--nitric.yaml
+--Pipfile
+--Pipfile.lock
+--README.md
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, in a new terminal window, you can run your application:

pipenv run dev
Enter fullscreen mode Exit fullscreen mode

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

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')
Enter fullscreen mode Exit fullscreen mode

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

@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.'}
Enter fullscreen mode Exit fullscreen mode

Retrieve a profile with GET

@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}"

Enter fullscreen mode Exit fullscreen mode

List all profiles with GET

@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}"
Enter fullscreen mode Exit fullscreen mode

Remove a profile with DELETE

@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.'}

Enter fullscreen mode Exit fullscreen mode

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.

Start the Nitric server to emulate cloud services on your machine:

nitric start
Enter fullscreen mode Exit fullscreen mode

Next, in a new terminal window, you can run your application:

pipenv run dev
Enter fullscreen mode Exit fullscreen mode

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:9001/apis/public/profiles' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "Peter Parker",
    "age": "21",
    "homeTown" : "Queens"
}'
Enter fullscreen mode Exit fullscreen mode

Fetch Profile

curl --location --request GET 'http://localhost:9001/apis/public/profiles/{id}'
Enter fullscreen mode Exit fullscreen mode

Fetch All Profiles

curl --location --request GET 'http://localhost:9001/apis/public/profiles'
Enter fullscreen mode Exit fullscreen mode

Delete Profile

curl --location --request DELETE 'http://localhost:9001/apis/public/profiles/{id}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
? 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
Enter fullscreen mode Exit fullscreen mode

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   |
└───────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

photos = bucket("photos").allow('reading','writing')
Enter fullscreen mode Exit fullscreen mode

Add imports for time and date so that we can set up caching/expiry headers

from datetime import datetime, timedelta
Enter fullscreen mode Exit fullscreen mode

Get a URL to upload a profile image

@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
Enter fullscreen mode Exit fullscreen mode

Get a URL to download a profile image

@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
Enter fullscreen mode Exit fullscreen mode

You can also directly redirect to the photo URL.

@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
Enter fullscreen mode Exit fullscreen mode

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:9001/apis/public/profiles/{id}/image/upload'
Enter fullscreen mode Exit fullscreen mode

Using the upload URL with curl

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

Enter fullscreen mode Exit fullscreen mode

Get an image download URL

curl --location --request GET 'http://localhost:9001/apis/public/profiles/{id}/image/download'
Enter fullscreen mode Exit fullscreen mode

Whats next?

As always we're open to your feedback! Let us know your thoughts.

You may also want to continue learning, try our Graphql guide.

Top comments (0)