How to build a notes API in Node.js
A notes API extends the basic CRUD pattern with user ownership — each note belongs to a specific user and only that user can read, update, or delete it.
As the creator of CoreUI with 25 years of backend development experience, I use this pattern as the template for any user-scoped resource API in Node.js.
The key difference from a public API is that every query includes a userId filter from the JWT payload, ensuring users can never access each other’s data.
This requires authentication middleware and a data model that links notes to users.
Define the Prisma schema with a User-Note relationship.
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
password String
notes Note[]
createdAt DateTime @default(now())
}
model Note {
id Int @id @default(autoincrement())
title String
content String
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
onDelete: Cascade automatically deletes all notes when the owning user is deleted. The userId foreign key links every note to its owner.
Notes Router with User Scoping
Filter all queries by the authenticated user’s ID.
// src/notes/notes.router.js
import { Router } from 'express'
import { PrismaClient } from '@prisma/client'
import { z } from 'zod'
const router = Router()
const prisma = new PrismaClient()
const noteSchema = z.object({
title: z.string().min(1).max(255),
content: z.string().min(1)
})
// GET /notes - list user's notes
router.get('/', async (req, res, next) => {
try {
const notes = await prisma.note.findMany({
where: { userId: req.user.id },
orderBy: { updatedAt: 'desc' }
})
res.json(notes)
} catch (err) {
next(err)
}
})
// POST /notes - create a note
router.post('/', async (req, res, next) => {
try {
const data = noteSchema.parse(req.body)
const note = await prisma.note.create({
data: { ...data, userId: req.user.id }
})
res.status(201).json(note)
} catch (err) {
next(err)
}
})
// PATCH /notes/:id - update user's note
router.patch('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id)
const data = noteSchema.partial().parse(req.body)
const note = await prisma.note.updateMany({
where: { id, userId: req.user.id },
data
})
if (note.count === 0) {
return res.status(404).json({ error: 'Note not found' })
}
res.json(await prisma.note.findUnique({ where: { id } }))
} catch (err) {
next(err)
}
})
// DELETE /notes/:id - delete user's note
router.delete('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id)
const result = await prisma.note.deleteMany({
where: { id, userId: req.user.id }
})
if (result.count === 0) {
return res.status(404).json({ error: 'Note not found' })
}
res.status(204).send()
} catch (err) {
next(err)
}
})
export { router as notesRouter }
Using findMany and deleteMany with both id and userId in the where clause prevents users from accessing other users’ notes. If the userId doesn’t match, count is 0 and you return a 404 — the same response as “not found” to avoid leaking whether other users’ notes exist.
Registering the Route
Mount the router behind the auth middleware.
// src/app.js
import express from 'express'
import { authMiddleware } from './auth/auth.middleware.js'
import { notesRouter } from './notes/notes.router.js'
const app = express()
app.use(express.json())
app.use('/notes', authMiddleware, notesRouter)
export { app }
Placing authMiddleware before notesRouter ensures every notes endpoint requires a valid JWT. The decoded user payload attached by the middleware is available as req.user in every route handler.
Best Practice Note
This is the same ownership pattern used throughout CoreUI backend templates for user-scoped resources. The updateMany/deleteMany + count check pattern is more efficient than a separate findFirst + authorization check because it combines the ownership check and the mutation in a single database query. For the simpler public CRUD pattern without user ownership, see how to build a todo API in Node.js.



