Skip to content

REST Naming Conventions

VeloxTS uses naming conventions to automatically generate REST endpoints from procedure names. Understanding these conventions is essential for predictable API design.

The procedure name prefix determines the HTTP method. The resource name (first argument to procedures()) determines the path.

// Resource: 'users' → Path: /api/users
export const userProcedures = procedures('users', {
listUsers: ..., // GET /api/users
getUser: ..., // GET /api/users/:id
createUser: ..., // POST /api/users
});
PrefixPathUse Case
list*/api/{resource}Get collection
get*/api/{resource}/:idGet single by ID
find*/api/{resource}Search/filter collection
procedures('products', {
listProducts: ..., // GET /api/products
getProduct: ..., // GET /api/products/:id
findProducts: ..., // GET /api/products (with query params)
});
PrefixPathStatus Code
create*/api/{resource}201 Created
add*/api/{resource}201 Created
procedures('users', {
createUser: ..., // POST /api/users → 201
addUser: ..., // POST /api/users → 201
});
PrefixPathUse Case
update*/api/{resource}/:idReplace entire resource
edit*/api/{resource}/:idReplace entire resource
procedures('posts', {
updatePost: ..., // PUT /api/posts/:id
editPost: ..., // PUT /api/posts/:id
});
PrefixPathUse Case
patch*/api/{resource}/:idUpdate specific fields
procedures('users', {
patchUser: ..., // PATCH /api/users/:id
});
PrefixPathStatus Code
delete*/api/{resource}/:id200 or 204
remove*/api/{resource}/:id200 or 204
procedures('posts', {
deletePost: ..., // DELETE /api/posts/:id
removePost: ..., // DELETE /api/posts/:id
});
PrefixHTTP MethodPath PatternResponse
get*GET/:idSingle resource
list*GET/Collection
find*GET/Filtered collection
create*POST/201 + created resource
add*POST/201 + created resource
update*PUT/:idUpdated resource
edit*PUT/:idUpdated resource
patch*PATCH/:idUpdated resource
delete*DELETE/:id200/204
remove*DELETE/:id200/204
procedures('users', {
listUsers: procedure()
.output(z.array(UserSchema))
.query(({ ctx }) => ctx.db.user.findMany()),
// → GET /api/users
getUser: procedure()
.input(z.object({ id: z.string() }))
.output(UserSchema)
.query(({ input, ctx }) => ctx.db.user.findUniqueOrThrow({ where: { id: input.id } })),
// → GET /api/users/:id
createUser: procedure()
.input(CreateUserSchema)
.output(UserSchema)
.mutation(({ input, ctx }) => ctx.db.user.create({ data: input })),
// → POST /api/users (201)
updateUser: procedure()
.input(z.object({ id: z.string(), data: UpdateUserSchema }))
.mutation(({ input, ctx }) => ctx.db.user.update({ where: { id: input.id }, data: input.data })),
// → PUT /api/users/:id
deleteUser: procedure()
.input(z.object({ id: z.string() }))
.mutation(({ input, ctx }) => ctx.db.user.delete({ where: { id: input.id } })),
// → DELETE /api/users/:id
});

Override conventions when they don’t fit:

procedures('auth', {
// Custom path → POST /api/auth/login
login: procedure()
.input(LoginSchema)
.rest({ method: 'POST', path: '/auth/login' })
.mutation(handler),
// Custom path with params → POST /api/auth/verify/:token
verifyEmail: procedure()
.input(z.object({ token: z.string() }))
.rest({ method: 'POST', path: '/auth/verify/:token' })
.mutation(handler),
// Disable REST entirely (tRPC-only)
internalOnly: procedure()
.rest({ enabled: false })
.query(handler),
});
procedures('users', {
// POST /api/users/bulk
createManyUsers: procedure()
.input(z.array(CreateUserSchema))
.rest({ method: 'POST', path: '/users/bulk' })
.mutation(({ input, ctx }) => ctx.db.user.createMany({ data: input })),
// DELETE /api/users/bulk
deleteManyUsers: procedure()
.input(z.object({ ids: z.array(z.string()) }))
.rest({ method: 'DELETE', path: '/users/bulk' })
.mutation(({ input, ctx }) => ctx.db.user.deleteMany({
where: { id: { in: input.ids } },
})),
});
procedures('users', {
// POST /api/users/:id/activate
activateUser: procedure()
.input(z.object({ id: z.string() }))
.rest({ method: 'POST', path: '/users/:id/activate' })
.mutation(({ input, ctx }) => ctx.db.user.update({
where: { id: input.id },
data: { isActive: true },
})),
// POST /api/users/:id/verify-email
verifyEmail: procedure()
.input(z.object({ id: z.string(), token: z.string() }))
.rest({ method: 'POST', path: '/users/:id/verify-email' })
.mutation(({ input, ctx }) => ctx.auth.verifyEmail(input.id, input.token)),
});
procedures('products', {
// GET /api/products?page=1&limit=20&category=electronics
listProducts: procedure()
.input(z.object({
page: z.coerce.number().default(1),
limit: z.coerce.number().default(20),
category: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const skip = (input.page - 1) * input.limit;
const [products, total] = await Promise.all([
ctx.db.product.findMany({
where: { category: input.category },
skip,
take: input.limit,
}),
ctx.db.product.count({ where: { category: input.category } }),
]);
return {
data: products,
meta: { page: input.page, limit: input.limit, total },
};
}),
});

VeloxTS automatically extracts URL path parameters and merges them with the input schema.

For procedures expecting :id (like getUser, updateUser, deleteUser):

getUser: procedure()
.input(z.object({ id: z.string() })) // Input schema has 'id'
.query(({ input, ctx }) => {
// input.id comes from URL: GET /api/users/abc-123
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
}),
// → GET /api/users/:id

Request: GET /api/users/abc-123 Handler receives: input = { id: "abc-123" }

For custom paths with multiple parameters:

getComment: procedure()
.input(z.object({
postId: z.string(),
commentId: z.string(),
}))
.rest({ method: 'GET', path: '/posts/:postId/comments/:commentId' })
.query(({ input }) => {
// input.postId = from URL
// input.commentId = from URL
}),

Request: GET /api/posts/post-1/comments/comment-5 Handler receives: input = { postId: "post-1", commentId: "comment-5" }

VeloxTS includes development-time warnings to catch naming convention issues.

No Convention Match:

// WARNING: "fetchUser" doesn't match any naming convention
fetchUser: procedure().query(...) // Should be: getUser

Type Mismatch:

// WARNING: "getUser" uses "get" prefix but is defined as mutation
getUser: procedure().mutation(...) // Should be .query()

Similar Name Detected:

// WARNING: "retrieveUser" - did you mean "getUser"?
retrieveUser: procedure().query(...)
// Disable warnings for specific namespace
export const legacyProcedures = procedures('legacy', procs, {
warnings: false,
});
// Strict mode (warnings become errors)
export const apiProcedures = procedures('api', procs, {
warnings: 'strict',
});
// Exclude specific procedures
export const mixedProcedures = procedures('users', procs, {
warnings: { except: ['customAction'] },
});
// BAD: GET prefix with mutation
getUserData: procedure()
.mutation(({ ctx }) => ctx.db.user.findMany()) // Should be .query()
// GOOD: Match type to prefix
listUsers: procedure()
.query(({ ctx }) => ctx.db.user.findMany())
// BAD: get* expects :id but no input
getUser: procedure()
.query(({ ctx }) => ctx.db.user.findMany()) // Returns all users!
// GOOD: Provide id input
getUser: procedure()
.input(z.object({ id: z.string() }))
.query(({ input, ctx }) => ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }))
// BAD: Convention already does this
getUser: procedure()
.rest({ method: 'GET', path: '/users/:id' }) // Redundant!
.query(...)
// GOOD: Let conventions work
getUser: procedure()
.query(...) // Auto-generates GET /api/users/:id

Causes: Procedure name doesn’t match convention, wrong type (query vs mutation), or incorrect casing.

Fix: Check dev console for warnings, use standard prefix, or add .rest() override.

Cause: Using .mutation() when you meant .query() or vice versa.

Rule of thumb:

  • get*, list*, find* → Always .query()
  • create*, add*, update*, edit*, patch*, delete*, remove* → Always .mutation()

listUsers and findUsers both generate GET /api/users. Override one with .rest() to avoid conflict:

listUsers: procedure()
.query(...),
// → GET /api/users (keep default)
findUsers: procedure()
.rest({ path: '/users/search' })
.query(...),
// → GET /api/users/search (override)