Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $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

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
Bootstrap 6: Everything You Need to Know (And Why CoreUI Is Already Ahead)
Bootstrap 6: Everything You Need to Know (And Why CoreUI Is Already Ahead)

How to check if an element is visible in JavaScript
How to check if an element is visible in JavaScript

What is globalThis in JavaScript?
What is globalThis in JavaScript?

How to change opacity on hover in CSS
How to change opacity on hover in CSS

Answers by CoreUI Core Team