Ship internal tools in hours, not weeks. Real auth, users, jobs, audit logs, and cohesive UI included. Early access $249 $499 → [Get it now]

How to build a todo API in Node.js

Building a todo API is the ideal first Node.js project because it covers every fundamental backend concept: creating, reading, updating, and deleting resources with a clean REST interface. As the creator of CoreUI with 25 years of backend development experience, I use this exact project structure as the starting point for more complex Node.js APIs. The key is using Prisma as the ORM for type-safe database access and adding input validation with Zod before any data touches the database. This combination gives you reliable data integrity and TypeScript-compatible queries from day one.

Set up the project with Express, Prisma, and Zod.

npm init -y
npm install express @prisma/client zod
npm install --save-dev prisma typescript @types/express
npx prisma init --datasource-provider sqlite
// prisma/schema.prisma
model Todo {
  id        Int      @id @default(autoincrement())
  title     String
  completed Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
npx prisma migrate dev --name init
npx prisma generate

SQLite is used here for simplicity — replace with postgresql for production. @updatedAt automatically sets the timestamp whenever a record changes.

The Todo Router

Define all CRUD routes in a dedicated router.

// src/todos/todos.router.js
import { Router } from 'express'
import { PrismaClient } from '@prisma/client'
import { z } from 'zod'

const router = Router()
const prisma = new PrismaClient()

const createSchema = z.object({
  title: z.string().min(1).max(200)
})

const updateSchema = z.object({
  title: z.string().min(1).max(200).optional(),
  completed: z.boolean().optional()
})

// GET /todos
router.get('/', async (req, res, next) => {
  try {
    const todos = await prisma.todo.findMany({ orderBy: { createdAt: 'desc' } })
    res.json(todos)
  } catch (err) {
    next(err)
  }
})

// POST /todos
router.post('/', async (req, res, next) => {
  try {
    const { title } = createSchema.parse(req.body)
    const todo = await prisma.todo.create({ data: { title } })
    res.status(201).json(todo)
  } catch (err) {
    next(err)
  }
})

// PATCH /todos/:id
router.patch('/:id', async (req, res, next) => {
  try {
    const id = Number(req.params.id)
    const data = updateSchema.parse(req.body)
    const todo = await prisma.todo.update({ where: { id }, data })
    res.json(todo)
  } catch (err) {
    next(err)
  }
})

// DELETE /todos/:id
router.delete('/:id', async (req, res, next) => {
  try {
    const id = Number(req.params.id)
    await prisma.todo.delete({ where: { id } })
    res.status(204).send()
  } catch (err) {
    next(err)
  }
})

export { router as todosRouter }

z.object().parse() throws a ZodError if validation fails. Pass the error to next(err) and your error handler will return a 400 response with the validation message. This keeps route handlers free of complex validation logic.

The Express App

Wire everything together.

// src/app.js
import express from 'express'
import { todosRouter } from './todos/todos.router.js'
import { errorHandler } from './middleware/error.handler.js'

const app = express()
app.use(express.json())
app.use('/todos', todosRouter)
app.use(errorHandler)

export { app }
// src/middleware/error.handler.js
import { ZodError } from 'zod'

export function errorHandler(err, req, res, next) {
  if (err instanceof ZodError) {
    return res.status(400).json({ error: err.errors })
  }
  if (err.code === 'P2025') {
    return res.status(404).json({ error: 'Todo not found' })
  }
  res.status(500).json({ error: 'Internal server error' })
}

P2025 is Prisma’s error code for “record not found”. Catching it in the error handler means you don’t need to add not-found checks in every route.

Best Practice Note

This is the same service structure we recommend in CoreUI Node.js project templates. For a production API, add pagination to the GET /todos route using skip and take query parameters, and add authentication middleware before the router. Once you’ve built the todo API, scale up to a notes API in Node.js which adds user ownership and filtering.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author