Documentation
API

Define Service

Add business logic in a reusable service file.

Create:

src/features/web/todo/service/todo.service.ts

Use the service layer for data access and feature logic.

Copy-paste scaffold

import prisma from '@/lib/prisma';

import type {
  CreateTodoInput,
  DeleteTodoInput,
  Todo,
  UpdateTodoInput,
} from '../model/todo.schema';

export class TodoNotFoundError extends Error {
  public readonly id: string;

  constructor(id: string) {
    super('Todo not found');
    this.name = 'TodoNotFoundError';
    this.id = id;
  }
}

export async function listTodos(): Promise<Todo[]> {
  return prisma.todo.findMany({
    orderBy: { createdAt: 'desc' },
    select: { id: true, title: true, completed: true },
  });
}

export async function createTodo(input: CreateTodoInput): Promise<Todo> {
  return prisma.todo.create({
    data: {
      title: input.title,
      completed: false,
    },
    select: { id: true, title: true, completed: true },
  });
}

export async function updateTodo(input: UpdateTodoInput): Promise<Todo> {
  const existing = await prisma.todo.findUnique({
    where: { id: input.id },
    select: { id: true },
  });

  if (!existing) {
    throw new TodoNotFoundError(input.id);
  }

  return prisma.todo.update({
    where: { id: input.id },
    data: {
      ...(input.title !== undefined ? { title: input.title } : {}),
      ...(input.completed !== undefined ? { completed: input.completed } : {}),
    },
    select: { id: true, title: true, completed: true },
  });
}

export async function deleteTodo(
  input: DeleteTodoInput,
): Promise<{ id: string }> {
  const existing = await prisma.todo.findUnique({
    where: { id: input.id },
    select: { id: true },
  });

  if (!existing) {
    throw new TodoNotFoundError(input.id);
  }

  await prisma.todo.delete({ where: { id: input.id } });
  return { id: input.id };
}

Notes

  • Keep service methods focused and reusable.
  • Throw domain errors (like TodoNotFoundError) and map them in the router.

On this page