Create new service functions for business logic and data access. Use when adding data operations, business logic, or database queries. Follows proper TypeScript patterns and return type conventions.
This skill guides you through creating properly structured service functions following clean architecture patterns.
All services go in: <services-dir>/[feature]-service.<ext>
Example structure:
<services-dir>/
├── portfolio-service.<ext>
├── transaction-service.<ext>
├── road-path-service.<ext>
└── auth-service.<ext>
import { db } from "<db-client>";
import { tableName } from "<schema-file>";
import { eq, and, desc } from "<orm>";
import type { TypeName } from "<types-dir>";
// GET operations (may not find data)
export async function getResourceById(
id: string
): Promise<TypeName | null> {
const result = await db.query.tableName.findFirst({
where: eq(tableName.id, id),
});
return result || null;
}
// GET operations (always return data)
export async function getAllResources(
userId: string
): Promise<TypeName[]> {
const results = await db.query.tableName.findMany({
where: eq(tableName.userId, userId),
orderBy: [desc(tableName.createdAt)],
});
return results;
}
// CREATE operations
export async function createResource(
userId: string,
data: CreateResourceData
): Promise<TypeName> {
const [resource] = await db
.insert(tableName)
.values({
id: crypto.randomUUID(),
userId,
...data,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return resource;
}
// UPDATE operations
export async function updateResource(
id: string,
userId: string,
data: UpdateResourceData
): Promise<TypeName> {
const [updated] = await db
.update(tableName)
.set({
...data,
updatedAt: new Date(),
})
.where(and(
eq(tableName.id, id),
eq(tableName.userId, userId) // Always verify ownership
))
.returning();
if (!updated) {
throw new Error("Resource not found or access denied");
}
return updated;
}
// DELETE operations
export async function deleteResource(
id: string,
userId: string
): Promise<void> {
await db
.delete(tableName)
.where(and(
eq(tableName.id, id),
eq(tableName.userId, userId) // Always verify ownership
));
}
Always define explicit return types:
// ✅ Good - explicit return type
export async function getUser(id: string): Promise<User | null> {
// ...
}
// ❌ Bad - inferred return type
export async function getUser(id: string) {
// ...
}
Promise<Type | null>Promise<Type[]>Promise<Type>Promise<void>Promise<{ success: boolean; data?: Type }>Never define types inline. Always import from <types-dir>:
// ✅ Good - imported type
import type { Portfolio } from "<types-dir>";
export async function getPortfolio(): Promise<Portfolio | null> { }
// ❌ Bad - inline type
export async function getPortfolio(): Promise<{
id: string;
// ... many fields
}> { }
Match ORM's nullable returns:
// In <types-dir>
export type RoadPath = {
id: string;
title: string;
description: string | null; // Nullable
startDate: Date | null; // Nullable
};
// In service
const path = await db.query.roadPaths.findFirst(...);
if (path?.startDate) {
const date = new Date(path.startDate);
}
Always verify user ownership:
export async function updateResource(
id: string,
userId: string, // ← Always require userId
data: UpdateData
): Promise<Resource> {
const [updated] = await db
.update(resources)
.set(data)
.where(and(
eq(resources.id, id),
eq(resources.userId, userId) // ← Verify ownership
))
.returning();
if (!updated) {
throw new Error("Resource not found or access denied");
}
return updated;
}
For multi-table operations:
export async function createWithRelated(
userId: string,
data: CreateData
): Promise<Result> {
return await db.transaction(async (tx) => {
const [parent] = await tx
.insert(parents)
.values({ userId, ...data.parent })
.returning();
const children = await tx
.insert(children)
.values(
data.children.map(child => ({
parentId: parent.id,
...child,
}))
)
.returning();
return { parent, children };
});
}
Follow these naming patterns:
getResourceById, getResourceBySluggetAllResources, getResourcesByFiltercreateResourceupdateResourcedeleteResourcecalculateTotal, formatResource, validateResource// <services-dir>/portfolio-service.<ext>
import { db } from "<db-client>";
import { portfolios, transactions } from "<schema-file>";
import { eq, and, desc, sum, sql } from "<orm>";
import type { Portfolio, Transaction } from "<types-dir>";
import type { CreatePortfolioData, UpdatePortfolioData } from "<schemas-dir>/portfolio";
export async function getPortfolioByUserId(
userId: string
): Promise<Portfolio | null> {
const portfolio = await db.query.portfolios.findFirst({
where: eq(portfolios.userId, userId),
with: {
transactions: {
orderBy: [desc(transactions.date)],
limit: 10,
},
},
});
return portfolio || null;
}
export async function createPortfolio(
userId: string,
data: CreatePortfolioData
): Promise<Portfolio> {
const [portfolio] = await db
.insert(portfolios)
.values({
id: crypto.randomUUID(),
userId,
...data,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning();
return portfolio;
}
export async function calculatePortfolioValue(
portfolioId: string
): Promise<number> {
const result = await db
.select({
total: sum(transactions.amount),
})
.from(transactions)
.where(eq(transactions.portfolioId, portfolioId));
return Number(result[0]?.total || 0);
}
Before completing a service:
<types-dir> and <schemas-dir>✅ Service file created in correct location ✅ All functions have explicit return types ✅ Types properly imported (not inline) ✅ Ownership verification in place ✅ Error handling implemented ✅ Transaction usage where needed ✅ Tests added (if required)
<services-dir>: Directory for service layer files<ext>: File extension (.ts, .js, etc.)<db-client>: Database client import path<schema-file>: Schema definitions import path<orm>: ORM library name<types-dir>: Shared types directory<schemas-dir>: Validation schemas directory<types-dir>