API's to update database - One POST path to update whole object or multiple?

I'm trying to understand what the common/best practice is for creating an API to update my database. Simple basic stuff, I'm just not familiar with what's common. If I have a Settings document/table and it has subsections like appearance, privacy, user_details, account_info, etc. Is it more common to... 1) Make the API mirror the prisma update function, so I'd create ONE REST API to the settings such that you can pass in any part of the settings and update the table? 2) Separate it out into the sections above so you have a REST API for each section so at least 4 POST routes to update the settings 3) Separate it out based on the UI, so there would maybe be 20 REST API's just to update each individual aspect of the settings? I like the idea of mirroring the prisma functions and just exposing it in a REST API (or trpc or whatever) but wanted to know if this was the typical approach
2 Replies
Yoers
Yoers15mo ago
I typically base my mutations around features. I.E a mutation called handleVote needs to update the User table to increment the post owner, and also the UserPost to add a new association of a vote against a post. You can then also have optional parameters, for example -
updateSelectedWidget: protectedProcedure
.input(
z.object({
widgetId: z.string(),
selectedDatabase: z.string().nullable().optional(),
selectedField: z.string().nullable().optional(),
})
)
.mutation(async ({ ctx, input }) => {
// If the user hasn't passed any widget ID, then look up first widget from database
const widgetId = input.widgetId;

// return first Widget associated to user
const res = await ctx.prisma.widget.findFirst({
where: {
id: widgetId,
},
});
if (!res) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to find existing widget",
});
} else {
// Update any fields that have been passed
const data = await ctx.prisma.widget.update({
where: {
id: widgetId,
},
data: {
selectedDatabase:
input.selectedDatabase === undefined
? res.selectedDatabase
: input.selectedDatabase,
selectedField:
input.selectedField === undefined
? res.selectedField
: input.selectedField,
},
});
if (!data) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update widget",
});
}
}

return null;
}),
updateSelectedWidget: protectedProcedure
.input(
z.object({
widgetId: z.string(),
selectedDatabase: z.string().nullable().optional(),
selectedField: z.string().nullable().optional(),
})
)
.mutation(async ({ ctx, input }) => {
// If the user hasn't passed any widget ID, then look up first widget from database
const widgetId = input.widgetId;

// return first Widget associated to user
const res = await ctx.prisma.widget.findFirst({
where: {
id: widgetId,
},
});
if (!res) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to find existing widget",
});
} else {
// Update any fields that have been passed
const data = await ctx.prisma.widget.update({
where: {
id: widgetId,
},
data: {
selectedDatabase:
input.selectedDatabase === undefined
? res.selectedDatabase
: input.selectedDatabase,
selectedField:
input.selectedField === undefined
? res.selectedField
: input.selectedField,
},
});
if (!data) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to update widget",
});
}
}

return null;
}),
jingleberry
jingleberry15mo ago
For user interactions, more granular mutations are preferred. For service to service calls a single endpoint which updates the entire object might be preferred. There’s no hard rule though