Skip to content

Adding a Service

Services hold business logic and database queries. They are called by server functions, never directly from components.

Add a new file in services/. Use named function exports, not classes:

src/services/notification.service.ts
import { db } from '#/database'
import { notifications } from '#/database/schemas/notifications'
import { eq, desc } from 'drizzle-orm'
export async function getNotifications(userId: string) {
return db
.select()
.from(notifications)
.where(eq(notifications.userId, userId))
.orderBy(desc(notifications.createdAt))
}
export async function markAsRead(notificationId: string) {
return db
.update(notifications)
.set({ readAt: new Date() })
.where(eq(notifications.id, notificationId))
}

Wrap the service in a server function with middleware:

src/functions/notifications.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { authMiddleware } from '#/middleware/auth'
import { createRateLimitMiddleware } from '#/middleware/rate-limit'
import * as notificationService from '#/services/notification.service'
const defaultRateLimit = createRateLimitMiddleware('default')
export const listNotifications = createServerFn({ method: 'GET' })
.middleware([defaultRateLimit, authMiddleware])
.handler(async ({ context }) => {
return notificationService.getNotifications(context.user.id)
})
export const markNotificationRead = createServerFn({ method: 'POST' })
.middleware([defaultRateLimit, authMiddleware])
.inputValidator(z.object({ id: z.string().min(1) }))
.handler(async ({ data }) => {
return notificationService.markAsRead(data.id)
})

Call the server function from a route loader and return its data. Components read via Route.useLoaderData(). See Adding a Page for the full pattern.

ThingConventionExample
Service file<domain>.service.tsnotification.service.ts
Service exportsNamed functionsgetNotifications, markAsRead
Server functioncamelCase verblistNotifications, markNotificationRead