Next.js and Nitric example application
This example application demonstrates a To-Do List built using Next.js for the frontend and Nitric for the API.
Prerequisites
Before getting started you'll want Node.js and the Nitric CLI installed. If you'd like to deploy the application to the cloud you'll also need an AWS, Google Cloud or Azure account. The code for the application will be the same regardless of the cloud you choose. You can also use Vercel or Netlify to deploy the frontend if you choose.
Getting started
Start by cloning the Nitric to-do project from GitHub and installing dependencies. The rest of this guide will walk you through the example and how it's built.
git clone https://github.com/nitrictech/nitric-todo.git
cd nitric-todo
npm install
Now you can open the project in your editor of choice.
Project structure
The project is split into two components:
- todo-api - This is the API built with Nitric
- web - This is the Next.js frontend
Define types
Since we're using TypeScript we've defined types for tasks and the api request/response payloads.
/* Base Types */
export interface Task {
id: string
createdAt: number
name: string
complete: boolean
description?: string
dueDate?: number
}
export interface TaskList {
id: string
createdAt: number
name: string
tasks: Task[]
}
/* Task List */
export type Filters = Partial<Task>
export type TaskListResponse = TaskList
export type TaskListRequest = Omit<TaskList, 'id' | 'tasks'>
export type TaskListPostRequest = Omit<TaskList, 'id' | 'complete'>
/* Task Post */
export type TaskPostRequest = Omit<Task, 'id'>
/* Task Update */
export type TaskPatchRequest = { completed: boolean }
Add cloud resources
Apps built with Nitric define resources in code, you can write this in the root of any .js
or .ts
file, but for organization we recommend putting them together. So let's start by defining the resources we'll need to support our API in a resources
directory.
import { api } from '@nitric/sdk'
export const taskListApi = api('taskList')
We also want a key/value store to store task lists.
import { kv } from '@nitric/sdk'
import { TaskList } from 'types'
export const taskLists = kv<TaskList>('taskLists')
Define API routes
Next, we setup API routes, these can remain as empty functions until we're ready to fill them in.
import { taskListApi } from '../resources/apis.ts';
taskListApi.get("/:listid/:id", async (ctx) => {}); // Get task with [id]
taskListApi.get("/:listid", async (ctx) => {); // Get task list with [id]
taskListApi.post("/:listid", async (ctx) => {}); // Post new task for task list
taskListApi.post("/", async (ctx) => {}); // Post new task list
taskListApi.patch("/:listid/:id", async (ctx) => {}); // Update task
taskListApi.delete("/:listid", async (ctx) => {}); // Delete task list
taskListApi.delete("/:listid/:id", async (ctx) => {}); // Delete task
Now we can import the task list key/value store and request permissions that allow this service to access it.
import { taskListCol } from '../resources/stores.ts'
const taskLists = taskListCol.for('setting', 'getting', 'deleting')
With access to the key/value store, we can start adding tasks and task lists.
Create a task list
taskListApi.post('/', async (ctx) => {
const { name, tasks } = ctx.req.json() as TaskListPostRequest
try {
if (!name) {
ctx.res.body = 'A new task list requires a name'
ctx.res.status = 400
return
}
const id = uuid.generate()
const now = new Date().getTime()
await taskLists.set(id, {
id,
name,
createdAt: now,
tasks: tasks.map(task => ({
...task,
complete: false,
createdAt: now,
})
})
ctx.res.body = 'Successfully added task list!'
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to add task list'
ctx.res.status = 400
}
return ctx
})
Create a new task
We can now accept task list ids and use them to add new tasks under that list.
taskListApi.post('/:listid', async (ctx) => {
const { listid } = ctx.req.params
const task = ctx.req.json() as TaskPostRequest
try {
if (!listid) {
ctx.res.body = 'A task list id is required'
ctx.res.status = 400
return
}
if (!task || !task.name) {
ctx.res.body = 'A task with a name is required'
ctx.res.status = 400
return
}
const taskId = uuid.generate()
const list = await taskLists.get(listid)
list = {
...list,
tasks: [
...list.tasks,
{
...task,
id: taskId,
complete: false,
createdAt: new Date().getTime(),
},
],
}
await taskLists.set(listid, list)
ctx.res.body = 'Successfully added task!'
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to add task list'
ctx.res.status = 400
}
return ctx
})
Retrieve a task with filters
// Get all tasks from a task list, with filters
taskListApi.get('/:listid', async (ctx) => {
const { listid } = ctx.req.params
const filters = ctx.req.query as Filters
try {
const { tasks } = await taskLists.get(listid)
const filteredTasks = tasks.filter((task) => {
return Object.entries(filters).every(([k, v]) => {
switch (k) {
case 'complete': {
return task[k] === v
}
case 'dueDate': {
return task[k] >= v
}
default: {
return task[k].startsWith(v)
}
}
})
})
ctx.res.json({
...taskList,
tasks: filteredTasks,
})
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to retrieve tasks'
ctx.res.status = 400
}
return ctx
})
Retrieve a task from a task list
taskListApi.get('/:listid/:id', async (ctx) => {
const { listid, id } = ctx.req.params
try {
const list = await taskLists.get(listid)
const task = list.tasks.find((task) => task.id === id)
ctx.res.json(task)
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to retrieve tasks'
ctx.res.status = 400
}
return ctx
})
Update a task
taskListApi.patch('/:listid/:id', async (ctx) => {
const { listid: listId, id } = ctx.req.params
const { completed } = ctx.req.json() as ToggleRequest
try {
const list = await taskLists.get(listId)
const task = list.tasks.find((task) => task.id === id)
task.complete = completed
await taskLists.set(listId, list)
ctx.res.body = 'Successfully updated task'
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to retrieve tasks'
ctx.res.status = 400
}
return ctx
})
Delete a task
taskListApi.delete('/:listid/:id', async (ctx) => {
const { listid: listId, id } = ctx.req.params
try {
const list = await taskLists.get(listId)
list.tasks = list.tasks.filter((task) => task.id !== id)
await taskLists.set(listId, list
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to delete task'
ctx.res.status = 400
}
return ctx
})
Delete a task list
taskListApi.delete('/:id', async (ctx) => {
const { id } = ctx.req.params
try {
await taskLists.delete(id)
ctx.res.body = 'Successfully deleted task list'
} catch (err) {
console.log(err)
ctx.res.body = 'Failed to delete task list'
ctx.res.status = 400
}
return ctx
})
Set up API proxy
To avoid any CORS issues we can use the Next.js backend as a proxy for the Nitric API. This is a quick way to ensure the API can be called from the same origin.
Start by create your .env
file by renaming the .env.example
file:
mv web/.env.example web/.env
Within the next.config.js
you should have rewrites defined to proxy between your universal Next.js API route and your Nitric APIs. It takes the API_BASE_URL
variable which is defined in the .env
file.
module.exports = {
reactStrictMode: true,
api: {
bodyParser: {
bodyParser: false, // Disallow body parsing, consume as stream
},
},
// To avoid any CORs issues use Next.js as a proxy for Nitric API
// We are working on it :)
async rewrites() {
return [
{
source: '/apis/:path*',
destination: `${process.env.API_BASE_URL}/:path*`, // Proxy to Backend
},
]
},
}
Running the application
Now that you have an API defined with handlers for each of the methods, we can test it out locally.
You can test the application with the npm run dev
command:
cd todo-api
npm run dev
the dev
script in the template starts the Nitric Server using nitric start
, then runs your functions.
We can then launch the Next.js frontend in a new terminal with:
cd ../web
npm run dev
Navigate to localhost:3000
to view the application. Alternatively, you can test the API directly at localhost:4001
using the Local Dashboard or any other HTTP client.
Pressing ctrl + a + k
will end the application.
Deploy to the cloud
Deploy the Nitric API
Setup credentials and cloud specific configuration:
Create a stack, which is a deployment target for your application.
nitric stack new
? What should we name this stack? todo
? Which provider do you want to deploy with? aws
? Which region should the stack deploy to? us-east-1
Then, you can deploy with the up
command.
nitric up
When the deployment is complete, go to the relevant cloud console to see and interact with the API.
To undeploy run the down
command:
nitric down
Deploy the Next.js App
Choose one of the following deploy buttons and make sure to update the API_BASE_URL
variable during the setup process with the deployed API URL.
The Netlify.toml
file in this repository includes the configuration for you
to customize the API_BASE_URL
property on the initial deploy.