Building a REST API with Nitric
This guide will show you how to build a serverless REST API with the Nitric framework using a JVM language like Java or Kotlin. 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:
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 |
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
- The Nitric CLI
- An AWS, Google Cloud or Azure account (your choice)
Getting started
Let's start by creating a new project from a Nitric template, this will provide a base to start building the API. In this case, you can choose the Java or Kotlin starter, examples for both are provided.
nitric new my-profile-api "official/Java - Starter (experimental)"
Next, open the project in your editor of choice.
cd my-profile-api
You can delete the common
, hello-service
, and goodbye-service
modules and instead structure your project similar to below (it will differ depending on Java or Kotlin):
gradle/
common/
src/
├── main/
├──── java/
├────── org/
├──────── example/
├────────── Main.java
.gitignore
build.gradle.kts
settings.gradle.kts
gradle.properties
gradlew
gradlew.bat
README.md
For JVM projects, the commands nitric up
and nitric run
expect that your project is compiled into a fat JAR file.
The build.gradle.kts
in the template project shows how to use the shadow jar plugin to create fat JARs. You can then run the gradle
task with:
./gradlew build
You can then the point to the compiled JARs in your handlers.
nitric.yaml
name: my-profile-api
handlers:
- 'build/libs/*.jar'
Create a Profile class
We will create a class to represent the profiles that we will store in the collection.
class Profile {
String name;
int age;
String homeTown;
public Profile (String name, int age, String homeTown) {
this.name = name;
this.age = age;
this.homeTown = homeTown;
}
}
Building the API
Applications built with Nitric can contain many APIs, let's start by adding one to this project to serve as the public endpoint. Delete the template code from the Application
code and add the following code to that file.
import io.nitric.Nitric;
import io.nitric.resources.CollectionPermission;
public class Application {
public static void main(String[] args) {
// Create an API named 'public'
var api = Nitric.INSTANCE.api("public");
// Define a collection named 'profiles', then request reading and writing permissions.
var profiles = Nitric.INSTANCE.collection("profiles").with(CollectionPermission.Write, CollectionPermission.Read);
}
}
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.
Resources in Nitric like api
and collection
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
collection
resource uses DynamoDB in AWS or FireStore on Google Cloud.
Create profiles with POST
Let's start adding 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.
import java.util.UUID;
...
profileApi.post("/profiles", (ctx) -> {
var id = UUID.randomUUID();
// Get the request JSON as a Profile object
var profileRequest = ctx.getReq().json(Profile.class);
// Store the new profile in the profiles collection
profiles.doc(id).set(profileRequest);
// Return the ID
ctx.getResp().text(id);
return ctx;
});
Retrieve a profile with GET
profileApi.get("/profiles/:id", (ctx) -> {
var id = ctx.getReq().getParams().get("id");
// Retrieve and return the profile data
var profile = profiles.doc(id).get();
if (profile == null) {
ctx.getResp().setStatus(404);
ctx.getResp().text(String.format("Profile with id '%s' not found", id));
return ctx;
}
ctx.getResp().json(profile);
return ctx;
});
List all profiles with GET
profileApi.get("/profiles", (ctx) -> {
var allProfiles = profiles.query().fetch();
ctx.getResp().json(allProfiles);
});
Remove a profile with DELETE
profileApi.delete("/profiles/:id", (ctx) -> {
var id = ctx.getReq().getParams().get("id");
// Delete the profile
try {
profiles.doc(id).delete();
} catch (Error err) {
ctx.getResp().setStatus(404);
ctx.getResp().text(String.format("Profile with id '%s' not found.", id));
}
return ctx;
});
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.
First start by running the nitric server using nitric start
, then use ./gradlew run
to run your project.
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
Cloud deployments incur costs and while most of these resource are available with free tier pricing you should consider the costs of the deployment.
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.
var profilesImg = Nitric.INSTANCE.bucket("profilesImg").with(BucketPermission.Read, BucketPermission.Write);
Get a URL to upload a profile image
profileApi.get("/profiles/:id/image/upload", (ctx) -> {
var id = ctx.getReq().getParams().get("id");
var photoName = String.format("images/%s/photo.png", id);
// Return a signed upload URL, which provides temporary access to upload a file.
var photoUrl = profilesImg.file(photoName).getUploadUrl();
ctx.getResp().text(photoUrl);
return ctx;
});
Get a URL to download a profile image
profileApi.get("/profiles/:id/image/download", (ctx) -> {
var id = ctx.getReq().getParams().get("id");
var photoName = String.format("images/%s/photo.png", id);
// Return a signed upload URL, which provides temporary access to download a file.
var photoUrl = profilesImg.file(photoName).getDownloadUrl();
ctx.getResp().text(photoUrl);
return ctx;
});
You can also return a redirect response that takes the HTTP client directly to the photo URL.
import java.util.Map;
import java.util.List;
...
profileApi.get("/profiles/:id/image/download", (ctx) -> {
var id = ctx.getReq().getParams().get("id");
var photoName = String.format("images/%s/photo.png", id);
// Redirect to a signed read-only file URL.
var photoUrl = profilesImg.file(photoName).getDownloadUrl();
ctx.getResp().setStatus(303);
ctx.getResp().setHeaders(Map.of("Location", List.of(photoUrl)));
return ctx;
});
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'