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 |
GET | profiles/[id]/image/view | View the image that is downloaded |
Prerequisites
- Go
- 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 my-profile-api go-starter
Next, open the project in your editor of choice.
cd my-profile-api
Make sure all dependencies are resolved:
go mod tidy
The scaffolded project should have the following structure:
+--services/
| +-- hello/
| +-- main.go
| ...
+--nitric.yaml
+--go.mod
+--go.sum
+--golang.dockerfile
+--.gitignore
+--README.md
You can test the project to verify everything is working as expected:
nitric start
If everything is working as expected you can now delete all files/folders in the services/
folder, we'll create new services in this guide.
Building the Profile API
Let's begin by setting up the Profiles API. First, create a new folder called profiles
within the services directory. Inside this folder, add a file named main.go
, and include the following code:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/nitrictech/go-sdk/handler"
"github.com/nitrictech/go-sdk/nitric"
)
func main() {
profilesApi, err := nitric.NewApi("public")
if err != nil {
return
}
profiles, err := nitric.NewKv("profiles").Allow(nitric.KvStoreGet, nitric.KvStoreSet)
if err != nil {
return
}
if err := nitric.Run(); err != nil {
fmt.Println(err)
}
}
Here we're creating:
- An API named
public
, - A key value store named
profiles
and giving our function permission to read and write to that key value store.
From here, let's add some features to that function that allow us to work with profiles.
You could separate some or all of these request handlers into their own services if you prefer. For simplicity we'll group them together in this guide.
Create profiles with POST
profilesApi.Post("/profiles", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
id := uuid.New().String()
var profileRequest map[string]interface{}
err := json.Unmarshal(ctx.Request.Data(), &profileRequest)
if err != nil {
return ctx, err
}
err = profiles.Set(context.Background(), id, profileRequest)
if err != nil {
return ctx, err
}
ctx.Response.Body = []byte(id)
return ctx, nil
})
Retrieve a profile with GET
profilesApi.Get("/profiles/:id", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
id := ctx.Request.PathParams()["id"]
profile, err := profiles.Get(context.Background(), id)
if err != nil {
ctx.Response.Status = 404
ctx.Response.Body = []byte(fmt.Sprintf("profile with id '%s' not found", id))
return ctx, nil
}
ctx.Response.Body, err = json.Marshal(profile)
return ctx, err
})
List all profiles with GET
profilesApi.Get("/profiles", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
keys, err := profiles.Keys(context.TODO())
if err != nil {
return ctx, err
}
var profileContent []map[string]interface{}
for {
key, err := keys.Recv()
if err != nil {
break
}
content, _ := profiles.Get(context.Background(), key)
profileContent = append(profileContent, content)
}
ctx.Response.Body, err = json.Marshal(profileContent)
return ctx, err
})
Remove a profile with DELETE
profilesApi.Delete("/profiles/:id", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
id := ctx.Request.PathParams()["id"]
err := profiles.Delete(context.Background(), id)
if err != nil {
ctx.Response.Status = 404
ctx.Response.Body = []byte(fmt.Sprintf("profile with id '%s' not found", id))
return ctx, nil
}
return ctx, nil
})
Do a quick go mod tidy
to make sure all new dependencies are resolved.
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, 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 services, just save them, they'll be reloaded automatically.
Test your API
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 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 key value store 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.
The stack new
command below will create a stack named dev
that uses the aws
provider.
nitric stack new dev aws
Continue by checking your stack file nitric.dev.yaml
and adding in your preferred region, let's use 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
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 profileImages
with read/write permissions
profileImages, err := nitric.NewBucket("profileImages").Allow(nitric.BucketRead, nitric.BucketWrite)
if err != nil {
fmt.Println(err)
return
}
Get a URL to upload a profile image
profilesApi.Get("/profiles/:id/image/upload", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
id := ctx.Request.PathParams()["id"]
photoId := fmt.Sprintf("images/%s/photo.png", id)
photoUrl, err := profileImages.File(photoId).UploadUrl(context.TODO(), 600)
if err != nil {
return ctx, err
}
ctx.Response.Body = []byte(photoUrl)
return ctx, nil
})
Get a URL to download a profile image
profilesApi.Get("/profiles/:id/image/download", func(ctx *handler.HttpContext, next handler.HttpHandler) (*handler.HttpContext, error) {
id := ctx.Request.PathParams()["id"]
photoId := fmt.Sprintf("images/%s/photo.png", id)
photoUrl, err := profileImages.File(photoId).DownloadUrl(context.TODO(), 600)
if err != nil {
return ctx, err
}
ctx.Response.Body = []byte(photoUrl)
return ctx, nil
})
You can also directly redirect to the photo URL.
profilesApi.Get("/profiles/:id/image/view", func(ctx *faas.HttpContext, next faas.HttpHandler) (*faas.HttpContext, error) {
id := ctx.Request.PathParams()["id"]
photoId := fmt.Sprintf("images/%s/photo.png", id)
photoUrl, err := profileImages.File(photoId).DownloadUrl(ctx.Request.Context(), 600)
if err != nil {
return ctx, err
}
ctx.Response.Status = 303
ctx.Response.Headers["Location"] = []string{photoUrl}
return ctx, nil
})
Time to test the updated 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'