API
Define Service
Add business logic in a reusable service file.
Create:
src/features/web/todo/service/todo.service.tsUse 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.