@Mutation Decorator
Learn how to create mutation procedures for creating, updating, and deleting data.
Basic Usage
Mutations are used for operations that modify data or cause side effects:
import { Mutation, Input, Context } from '@nexica/nestjs-trpc'
import { z } from 'zod'
@Router()
export class UserRouter {
@Mutation({
input: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).max(120),
}),
output: UserSchema,
})
async createUser(@Input() input: { name: string; email: string; age: number }) {
return await this.userService.create(input)
}
@Mutation({
input: z.object({
id: z.string(),
data: z.object({
name: z.string().optional(),
email: z.string().email().optional(),
}),
}),
output: UserSchema,
})
async updateUser(@Input() input: { id: string; data: { name?: string; email?: string } }) {
return await this.userService.update(input.id, input.data)
}
@Mutation({
input: z.object({
id: z.string(),
}),
output: z.boolean(),
})
async deleteUser(@Input() input: { id: string }) {
await this.userService.delete(input.id)
return true
}
}
Configuration Options
The @Mutation()
decorator accepts the following options:
Option | Type | Description |
---|---|---|
input | ZodSchema | Input validation schema (optional) |
output | ZodSchema | Output validation schema (optional) |
meta | object | Metadata for the procedure (optional) |
Create Operations
Handle data creation with comprehensive validation:
const CreateUserSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name too long'),
email: z.string()
.email('Invalid email format')
.toLowerCase(),
age: z.number()
.int('Age must be an integer')
.min(18, 'Must be at least 18')
.max(120, 'Invalid age'),
preferences: z.object({
newsletter: z.boolean().default(false),
theme: z.enum(['light', 'dark']).default('light'),
}).optional(),
})
@Mutation({
input: CreateUserSchema,
output: UserSchema,
})
async createUser(@Input() input: z.infer<typeof CreateUserSchema>) {
// Check for existing user
const existing = await this.userService.findByEmail(input.email)
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'User with this email already exists',
})
}
return await this.userService.create(input)
}
Update Operations
Handle partial updates with validation:
const UpdateUserSchema = z.object({
id: z.string().uuid(),
data: z.object({
name: z.string().min(1).max(100).optional(),
email: z.string().email().toLowerCase().optional(),
age: z.number().int().min(18).max(120).optional(),
preferences: z.object({
newsletter: z.boolean().optional(),
theme: z.enum(['light', 'dark']).optional(),
}).optional(),
}),
})
@Mutation({
input: UpdateUserSchema,
output: UserSchema,
})
async updateUser(@Input() input: z.infer<typeof UpdateUserSchema>, @Context() ctx: RequestContext) {
// Verify user exists and user has permission
const existingUser = await this.userService.findById(input.id)
if (!existingUser) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
})
}
// Check authorization (user can only update their own profile, or admin)
if (ctx.userId !== input.id && !ctx.isAdmin) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Not authorized to update this user',
})
}
// Check email uniqueness if email is being updated
if (input.data.email && input.data.email !== existingUser.email) {
const emailExists = await this.userService.findByEmail(input.data.email)
if (emailExists) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Email already in use',
})
}
}
return await this.userService.update(input.id, input.data)
}
Delete Operations
Handle data deletion with proper authorization:
@Mutation({
input: z.object({
id: z.string().uuid(),
}),
output: z.object({
success: z.boolean(),
message: z.string(),
}),
})
async deleteUser(@Input() input: { id: string }, @Context() ctx: RequestContext) {
// Verify user exists
const user = await this.userService.findById(input.id)
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'User not found',
})
}
// Check authorization
if (ctx.userId !== input.id && !ctx.isAdmin) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Not authorized to delete this user',
})
}
// Prevent self-deletion for admins (optional business rule)
if (ctx.userId === input.id && ctx.isAdmin) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Admins cannot delete their own account',
})
}
await this.userService.delete(input.id)
return {
success: true,
message: 'User deleted successfully',
}
}
Batch Operations
Handle multiple operations efficiently:
@Mutation({
input: z.object({
users: z.array(CreateUserSchema).max(100, 'Cannot create more than 100 users at once'),
}),
output: z.object({
created: UserSchema.array(),
errors: z.array(z.object({
index: z.number(),
error: z.string(),
})),
}),
})
async createUsers(@Input() input: { users: z.infer<typeof CreateUserSchema>[] }) {
const created: User[] = []
const errors: { index: number; error: string }[] = []
for (let i = 0; i < input.users.length; i++) {
try {
const user = input.users[i]
// Check for existing email
const existing = await this.userService.findByEmail(user.email)
if (existing) {
errors.push({
index: i,
error: `Email ${user.email} already exists`,
})
continue
}
const newUser = await this.userService.create(user)
created.push(newUser)
} catch (error) {
errors.push({
index: i,
error: error instanceof Error ? error.message : 'Unknown error',
})
}
}
return { created, errors }
}
Error Handling
Handle errors consistently in mutation procedures:
import { TRPCError } from '@trpc/server'
@Mutation({
input: z.object({ email: z.string().email() }),
output: UserSchema,
})
async createUser(@Input() input: { email: string }) {
try {
// Check for existing user
const existing = await this.userService.findByEmail(input.email)
if (existing) {
throw new TRPCError({
code: 'CONFLICT',
message: 'User with this email already exists',
})
}
return await this.userService.create(input)
} catch (error) {
if (error instanceof TRPCError) {
throw error // Re-throw tRPC errors
}
// Handle database constraint violations
if (error.code === '23505') { // PostgreSQL unique violation
throw new TRPCError({
code: 'CONFLICT',
message: 'User already exists',
})
}
// Handle unexpected errors
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create user',
cause: error,
})
}
}
Using with Middleware
Apply authentication and authorization to mutations:
import { Middleware } from '@nexica/nestjs-trpc'
import { AuthMiddleware, AdminMiddleware, RateLimitMiddleware } from '@/middleware'
@Router()
export class UserRouter {
@Mutation()
@Middleware(RateLimitMiddleware)
async registerUser(@Input() input: { email: string; password: string }) {
// Rate limited registration - no auth required
return await this.userService.register(input)
}
@Mutation()
@Middleware(AuthMiddleware)
async updateMyProfile(@Input() input: { name: string }, @Context() ctx: RequestContext) {
// Auth required - users can update their own profile
return await this.userService.update(ctx.userId, input)
}
@Mutation()
@Middleware(AuthMiddleware, AdminMiddleware)
async deleteUser(@Input() input: { id: string }) {
// Admin access required
return await this.userService.delete(input.id)
}
}
Transaction Handling
Use database transactions for complex operations:
@Mutation({
input: z.object({
user: CreateUserSchema,
profile: z.object({
bio: z.string().optional(),
avatar: z.string().url().optional(),
}),
}),
output: z.object({
user: UserSchema,
profile: ProfileSchema,
}),
})
async createUserWithProfile(@Input() input: { user: any; profile: any }) {
// Use database transaction to ensure atomicity
return await this.databaseService.transaction(async (trx) => {
// Create user first
const user = await this.userService.create(input.user, trx)
// Then create profile
const profile = await this.profileService.create({
userId: user.id,
...input.profile,
}, trx)
return { user, profile }
})
}
Transaction Best Practices
- Keep transactions short and focused - Avoid long-running operations within transactions - Handle deadlocks gracefully with retry logic - Use appropriate isolation levels for your use case
Best Practices
- Validate inputs thoroughly - Use comprehensive Zod schemas with clear error messages
- Check authorization - Verify user permissions before performing operations
- Handle conflicts - Check for duplicates and constraint violations
- Use transactions - Ensure data consistency for multi-step operations
- Provide meaningful responses - Return useful information about the operation result
- Implement idempotency - Allow safe retries for critical operations
- Rate limiting - Protect against abuse with appropriate rate limits
Next Steps
- Queries - Learn about reading data safely
- Input Decorator - Master input parameter handling
- Context Decorator - Access request context and authentication
- Middleware - Add authentication and validation layers
Last updated on: