APIs
Node.js enables developers to build scalable server-side applications with JavaScript. Its event-driven architecture makes it ideal for APIs. While several frameworks exist — such as Express, Koa, and Fastify — this guide uses Hono, a lightweight and fast Node.js web framework.
We will be creating a simple API that will allow you to update a list of countries and items associated with each country. The data will be stored in a PostgreSQL database, and we will use Drizzle ORM to interact with the database.
This guide is comprised of 4 main steps:
- Setting up a new Node.js API project
- Setting up a dedicated PostgreSQL database for the API
- Migrating and seeding the database using Drizzle ORM
- Deploying the API to lttle.cloud
We will publish this API and allow everybody to access it. Because of this we will not add any POST or DELETE routes that will create or delete data. We will add GET and PUT routes only for reading and updating data.
Initializing the project​
First, we need to create a new Node.js project for our API. You can use any runtime or framework you prefer, but for this example, we will use Node.js with Hono for its simplicity and performance.
- npm
- Yarn
- pnpm
- Bun
npm create hono@latest hono-api
yarn create hono hono-api
pnpm create hono hono-api
bunx create-hono hono-api
And select the nodejs template when prompted.
Setting up PostgreSQL​
For local development, you can use Docker to run a PostgreSQL instance:
services:
pg:
image: ghcr.io/lttle-cloud/postgres:17-flash
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: db
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:
Run the PostgreSQL container:
docker-compose up -d
And add the following to your .env file:
DATABASE_URL=postgresql://postgres:password@localhost:5432/db
For more details related to PostgreSQL on lttle.cloud, check out our PostgreSQL guide.
Setting up Drizzle ORM​
Here we recommend following the Get Started with Drizzle and PostgreSQL to set up Drizzle ORM in your project.
First, install the required dependencies:
- npm
- Yarn
- pnpm
- Bun
npm install drizzle-orm pg
npm install -D drizzle-kit
yarn add drizzle-orm pg
yarn add --dev drizzle-kit
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit
bun add drizzle-orm pg
bun add --dev drizzle-kit
Then, create a drizzle.config.ts file in the root of your project:
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Database schema​
Next, create the database schema in src/db/schema.ts:
import { integer, pgTable, text } from "drizzle-orm/pg-core";
export const list = pgTable("list", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
name: text("name").notNull(),
});
export const item = pgTable("item", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
description: text("description").notNull(),
listId: integer("list_id")
.notNull()
.references(() => list.id),
});
Migrations​
Now, create the initial migration:
- npm
- Yarn
- pnpm
- Bun
npx drizzle-kit generate
yarn dlx drizzle-kit generate
pnpm dlx drizzle-kit generate
bun x drizzle-kit generate
Run the migration to create the tables in the database:
- npm
- Yarn
- pnpm
- Bun
npx drizzle-kit migrate
yarn dlx drizzle-kit migrate
pnpm dlx drizzle-kit migrate
bun x drizzle-kit migrate
Seeding the database​
Finally, create a seed script in src/db/seed.ts to populate the database with initial data:
import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import { item, list } from "./schema";
async function main() {
const db = drizzle(process.env.DATABASE_URL!);
await seed(db, { list, item }).refine((f) => ({
list: {
columns: {
name: f.country(),
},
count: 2,
with: {
item: 3,
},
},
item: {
columns: {
description: f.loremIpsum({ sentencesCount: 1 }),
},
count: 3,
},
}));
}
main();
Run the seed script:
- npm
- Yarn
- pnpm
- Bun
npx tsx src/db/seed.ts
yarn dlx tsx src/db/seed.ts
pnpm dlx tsx src/db/seed.ts
bun x tsx src/db/seed.ts
To verify that the data has been inserted correctly, you can use Drizzle Studio or any PostgreSQL client of your choice.
- npm
- Yarn
- pnpm
- Bun
npx drizzle-studio
yarn dlx drizzle-studio
pnpm dlx drizzle-studio
bun x drizzle-studio
Database Querying​
To interact with the database in our API routes, we need to set up a Drizzle ORM instance. Create a new file src/db/client.ts:
import { eq } from "drizzle-orm";
import { db } from "./drizzle";
import { item, list } from "./schema";
export const getFullList = async () => {
return db.select().from(list);
};
export function getListItems(id: number) {
return db.select().from(item).where(eq(item.listId, id));
}
export const getListItem = async (id: number) => {
const items = await db.select().from(item).where(eq(item.id, id)).limit(1);
return items[0];
};
export const updateListItem = async (id: number, description: string) => {
return db.update(item).set({ description }).where(eq(item.id, id));
};
Here, we have defined three functions:
getFullList: Fetches all lists from the database.getListItems: Fetches all items associated with a specific list.updateListItem: Updates the description of a specific item.
Adding our API routes​
Open the src/index.ts file and modify it as follows:
import { getListItems, getFullList, updateListItem } from "./db/client";
app.get("/", (c) => {
return c.text("Hello Hono from lttle.cloud!");
});
app.get("/lists", async (c) => {
const lists = await getFullList();
return c.json(lists);
});
app.get("/lists/:id/items", async (c) => {
const id = Number(c.req.param("id"));
const items = await getListItems(id);
return c.json(items);
});
app.get("/items/:id", async (c) => {
const id = Number(c.req.param("id"));
const item = await getListItem(id);
return c.json(item);
});
app.put("/items/:id", async (c) => {
const id = Number(c.req.param("id"));
const { description } = await c.req.json();
await updateListItem(id, description);
return c.json({ message: "Item updated" });
});
Testing the API locally​
You can test the API locally by running the development server:
- npm
- Yarn
- pnpm
- Bun
npm run dev
yarn dev
pnpm run dev
bun run dev
Now we can test our API endpoints using a tool like Postman or curl.
curl -s http://localhost:3000/lists | jq
[
{
"id": 1,
"name": "Tanzania"
},
{
"id": 2,
"name": "Ghana"
}
]
To get the list items for a specific list, we can use the following GET request:
curl -s http://localhost:3000/lists/1/items | jq
[
{
"id": 4,
"description": "Nulla non dapibus nibh, id ultricies augue. ",
"listId": 1
},
{
"id": 5,
"description": "Integer pretium pulvinar sem, eget vehicula sem egestas vel. ",
"listId": 1
},
{
"id": 6,
"description": "Integer mattis egestas tellus, et volutpat ligula placerat non. ",
"listId": 1
}
]
If we want to update an item, we can use the following PUT request:
curl -s -X PUT http://localhost:3000/items/4 \
-H 'Content-Type: application/json' \
-d '{"description":"Updated description"}' | jq
{
"message": "Item updated"
}
And if we want to see the list item directly with its new description:
curl -s http://localhost:3001/items/4 | jq
{
"id": 4,
"description": "Updated description",
"listId": 1
}
Deploying to lttle.cloud​
Based on this project structure we have 4 things we need to deploy
- A volume for the database data
- PostgreSQL database app
- The Node.js Hono API app that will expose via a service definition the API publicly
- A machine that will migrate the database on startup
- A machine that will seed the database on startup
It should look something like this in the lttle.yaml file:
volume:
name: hono-api-pgdata
namespace: samples
mode: writeable
size: 100Mi
---
app:
name: hono-api-pg
namespace: samples
image: ghcr.io/lttle-cloud/postgres:17-flash
resources:
cpu: 1
memory: 256
mode:
flash:
strategy: manual
volumes:
- name: hono-api-pgdata
namespace: samples
path: /var/lib/postgresql/data
environment:
POSTGRES_DB: postgres
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
expose:
internal:
port: 5432
internal: {}
connection-tracking:
traffic-aware:
inactivity-timeout: 3
---
app:
name: hono-api
namespace: samples
build: auto
resources:
cpu: 1
memory: 256
mode:
flash:
strategy:
listen-on-port: 3000
environment:
DATABASE_URL: postgresql://postgres:password@hono-api-pg-internal.samples.svc.lttle.local:5432/postgres
depends-on:
- name: hono-api-pg
namespace: samples
expose:
public:
port: 3000
external:
protocol: https
---
machine:
name: hono-api-drizzle-migrate
namespace: samples
build:
docker:
context: .
dockerfile: drizzle.dockerfile
command:
- npm
- run
- migrate
resources:
cpu: 1
memory: 512
restart-policy: remove
depends-on:
- name: hono-api
namespace: samples
- name: hono-api-pg
namespace: samples
environment:
DATABASE_URL: postgresql://postgres:password@hono-api-pg-internal.samples.svc.lttle.local:5432/postgres
---
machine:
name: hono-api-drizzle-seed
namespace: samples
build:
docker:
context: .
dockerfile: drizzle.dockerfile
command:
- npm
- run
- seed
resources:
cpu: 1
memory: 512
restart-policy: remove
depends-on:
- name: hono-api-drizzle-migrate
environment:
DATABASE_URL: postgresql://postgres:password@hono-api-pg-internal.samples.svc.lttle.local:5432/postgres