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 Core Rule
Section titled “The Core Rule”The procedure name prefix determines the HTTP method. The resource name (first argument to procedures()) determines the path.
// Resource: 'users' → Path: /api/usersexport const userProcedures = procedures('users', { listUsers: ..., // GET /api/users getUser: ..., // GET /api/users/:id createUser: ..., // POST /api/users});Naming Patterns
Section titled “Naming Patterns”GET - Read Operations
Section titled “GET - Read Operations”| Prefix | Path | Use Case |
|---|---|---|
list* | /api/{resource} | Get collection |
get* | /api/{resource}/:id | Get 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)});POST - Create Operations
Section titled “POST - Create Operations”| Prefix | Path | Status Code |
|---|---|---|
create* | /api/{resource} | 201 Created |
add* | /api/{resource} | 201 Created |
procedures('users', { createUser: ..., // POST /api/users → 201 addUser: ..., // POST /api/users → 201});PUT - Full Update
Section titled “PUT - Full Update”| Prefix | Path | Use Case |
|---|---|---|
update* | /api/{resource}/:id | Replace entire resource |
edit* | /api/{resource}/:id | Replace entire resource |
procedures('posts', { updatePost: ..., // PUT /api/posts/:id editPost: ..., // PUT /api/posts/:id});PATCH - Partial Update
Section titled “PATCH - Partial Update”| Prefix | Path | Use Case |
|---|---|---|
patch* | /api/{resource}/:id | Update specific fields |
procedures('users', { patchUser: ..., // PATCH /api/users/:id});DELETE - Remove
Section titled “DELETE - Remove”| Prefix | Path | Status Code |
|---|---|---|
delete* | /api/{resource}/:id | 200 or 204 |
remove* | /api/{resource}/:id | 200 or 204 |
procedures('posts', { deletePost: ..., // DELETE /api/posts/:id removePost: ..., // DELETE /api/posts/:id});Complete Reference Table
Section titled “Complete Reference Table”| Prefix | HTTP Method | Path Pattern | Response |
|---|---|---|---|
get* | GET | /:id | Single resource |
list* | GET | / | Collection |
find* | GET | / | Filtered collection |
create* | POST | / | 201 + created resource |
add* | POST | / | 201 + created resource |
update* | PUT | /:id | Updated resource |
edit* | PUT | /:id | Updated resource |
patch* | PATCH | /:id | Updated resource |
delete* | DELETE | /:id | 200/204 |
remove* | DELETE | /:id | 200/204 |
Examples
Section titled “Examples”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});procedures('products', { // Collection endpoint listProducts: procedure() .output(z.array(ProductSchema)) .query(({ ctx }) => ctx.db.product.findMany()), // → GET /api/products
// Search with query params findProducts: procedure() .input(z.object({ category: z.string().optional(), minPrice: z.coerce.number().optional(), maxPrice: z.coerce.number().optional(), })) .output(z.array(ProductSchema)) .query(({ input, ctx }) => ctx.db.product.findMany({ where: { category: input.category, price: { gte: input.minPrice, lte: input.maxPrice, }, }, })), // → GET /api/products?category=electronics&minPrice=100});Custom Routes with .rest()
Section titled “Custom Routes with .rest()”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),});Common Patterns
Section titled “Common Patterns”Bulk Operations
Section titled “Bulk Operations”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 } }, })),});Custom Actions
Section titled “Custom Actions”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)),});Pagination
Section titled “Pagination”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 }, }; }),});URL Parameter Extraction
Section titled “URL Parameter Extraction”VeloxTS automatically extracts URL path parameters and merges them with the input schema.
How It Works
Section titled “How It Works”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/:idRequest: GET /api/users/abc-123
Handler receives: input = { id: "abc-123" }
Multiple Path Parameters
Section titled “Multiple Path Parameters”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" }
Query Parameter Coercion
Section titled “Query Parameter Coercion”Warning System
Section titled “Warning System”VeloxTS includes development-time warnings to catch naming convention issues.
Warning Types
Section titled “Warning Types”No Convention Match:
// WARNING: "fetchUser" doesn't match any naming conventionfetchUser: procedure().query(...) // Should be: getUserType Mismatch:
// WARNING: "getUser" uses "get" prefix but is defined as mutationgetUser: procedure().mutation(...) // Should be .query()Similar Name Detected:
// WARNING: "retrieveUser" - did you mean "getUser"?retrieveUser: procedure().query(...)Configure Warnings
Section titled “Configure Warnings”// Disable warnings for specific namespaceexport const legacyProcedures = procedures('legacy', procs, { warnings: false,});
// Strict mode (warnings become errors)export const apiProcedures = procedures('api', procs, { warnings: 'strict',});
// Exclude specific proceduresexport const mixedProcedures = procedures('users', procs, { warnings: { except: ['customAction'] },});Anti-Patterns
Section titled “Anti-Patterns”Wrong Procedure Type
Section titled “Wrong Procedure Type”// BAD: GET prefix with mutationgetUserData: procedure() .mutation(({ ctx }) => ctx.db.user.findMany()) // Should be .query()
// GOOD: Match type to prefixlistUsers: procedure() .query(({ ctx }) => ctx.db.user.findMany())Missing ID for get* Pattern
Section titled “Missing ID for get* Pattern”// BAD: get* expects :id but no inputgetUser: procedure() .query(({ ctx }) => ctx.db.user.findMany()) // Returns all users!
// GOOD: Provide id inputgetUser: procedure() .input(z.object({ id: z.string() })) .query(({ input, ctx }) => ctx.db.user.findUniqueOrThrow({ where: { id: input.id } }))Unnecessary .rest() Override
Section titled “Unnecessary .rest() Override”// BAD: Convention already does thisgetUser: procedure() .rest({ method: 'GET', path: '/users/:id' }) // Redundant! .query(...)
// GOOD: Let conventions workgetUser: procedure() .query(...) // Auto-generates GET /api/users/:idTroubleshooting
Section titled “Troubleshooting”REST endpoint returns 404
Section titled “REST endpoint returns 404”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.
Wrong HTTP method generated
Section titled “Wrong HTTP method generated”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()
Multiple procedures map to same endpoint
Section titled “Multiple procedures map to same endpoint”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)Next Steps
Section titled “Next Steps”- REST Overrides - Custom path configuration
- Validation Coercion - Type conversion patterns
- OpenAPI - API documentation