8 min read
Building Type-Safe APIs with Nuxt 3 and Prisma
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
1.9K
Move beyond try-catch: Result types, error boundaries, and structured error handling that makes your TypeScript apps more resilient.
Mbeah Essilfie
June 3, 2026 at 06:03 AM
Try-catch is invisible in the type system. There's no way to know what errors a function might throw:
// What errors can this throw? No way to know from the signature.
async function getUser(id: string): Promise<User> {
const user = await db.user.findUnique({ where: { id } })
if (!user) throw new Error('Not found')
return user
}
Encode success and failure in the return type:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
function ok<T>(value: T): Result<T, never> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
Now errors are visible in the type system:
type UserError = 'NOT_FOUND' | 'PERMISSION_DENIED'
async function getUser(id: string): Promise<Result<User, UserError>> {
const user = await db.user.findUnique({ where: { id } })
if (!user) return err('NOT_FOUND')
return ok(user)
}
// Caller MUST handle both cases
const result = await getUser('123')
if (!result.ok) {
// result.error is typed as UserError
switch (result.error) {
case 'NOT_FOUND': return notFound()
case 'PERMISSION_DENIED': return forbidden()
}
}
// result.value is typed as User
console.log(result.value.name)
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public details?: unknown,
) {
super(message)
this.name = 'AppError'
}
static notFound(resource: string) {
return new AppError(`${resource} not found`, 'NOT_FOUND', 404)
}
static validation(details: Record<string, string>) {
return new AppError('Validation failed', 'VALIDATION', 422, details)
}
static unauthorized() {
return new AppError('Unauthorized', 'UNAUTHORIZED', 401)
}
}
// server/middleware/error-handler.ts
export default defineEventHandler((event) => {
event.node.res.on('error', (error) => {
if (error instanceof AppError) {
sendError(event, createError({
statusCode: error.statusCode,
message: error.message,
data: { code: error.code, details: error.details },
}))
}
})
})
// Wrap fetch to return Results
async function safeFetch<T>(url: string): Promise<Result<T, FetchError>> {
try {
const response = await fetch(url)
if (!response.ok) {
return err({ type: 'HTTP_ERROR', status: response.status })
}
const data = await response.json()
return ok(data as T)
} catch (e) {
return err({ type: 'NETWORK_ERROR', message: (e as Error).message })
}
}
Fullstack Software Developer
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
Go beyond basic utility classes and learn how to build cohesive, maintainable design systems with Tailwind CSS.
Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.
