Create Hooks
Create hooks allow you to intercept Create, CreateMany, and CreateManyAndReturn operations before queries hit the database. You can validate inputs, mutate fields, handle upsert conflicts, or short-circuit query execution entirely.
Schema Context
The examples in this guide reference the following Prisma schema:
model User {
id String @id @default(cuid())
email String @unique
phoneNum String @unique
password String?
role UserRole @default(STUDENT)
loginCount Int @default(0)
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
published Boolean @default(false)
authorId String?
}Registration & Chaining
Register hooks on a model delegate using .Use(), passing a model extension (e.g. user.Extension):
db.User.Use(user.Extension{
Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) {
// Pre-hook: runs before database execution
args.Data.Email = strings.ToLower(args.Data.Email)
res, err := next(ctx, args)
// Post-hook: runs after database execution
return res, err
},
})Chaining Order & Context Flow
Multiple .Use() calls stack in middleware order (outermost to innermost):
Request → Hook A (Pre) → Hook B (Pre) → Database → Hook B (Post) → Hook A (Post) → ResponseNote: Context values set via
context.WithValueflow inward to subsequent hooks and query execution, but are isolated from outer hooks during unwinding.
Hook Signatures
| Hook Field | Signature | Return Type |
|---|---|---|
Create |
func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) |
(*phi.User, error) |
CreateMany |
func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) |
(int64, error) |
CreateManyAndReturn |
func(ctx context.Context, args *phi.UserCreateManyAndReturnArgs, next phi.UserCreateManyAndReturnQuery) |
([]*phi.User, error) |
1. Single Record (`Create`)
*phi.UserCreateArgs exposes the following query properties:
| Field | Type | Description |
|---|---|---|
Data |
*phi.UserCreate |
The record fields being inserted. |
Select |
*phi.UserSelect |
Selected scalar and relation fields to return. |
ConflictTarget |
phi.UniqueConstraintTarget |
Unique column or composite key for upserts. |
ConflictAction |
*phi.ConflictAction |
Resolution action (DoNothing, UpdateNewValues, or custom). |
Mutating Inputs
Modify args.Data directly before invoking next:
db.User.Use(user.Extension{
Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) {
// Normalize email
args.Data.Email = strings.ToLower(strings.TrimSpace(args.Data.Email))
// Hash password if present
if args.Data.Password != nil {
hashed := hashPassword(*args.Data.Password)
args.Data.Password = &hashed
}
return next(ctx, args)
},
})Note: Required fields are concrete types, as they must be provided, no need for
nilcheck, optional fields are always pointers, as they may benilto indicate "unset".
Short-Circuiting Execution
Return early without calling next(ctx, args) to bypass database insertion:
db.User.Use(user.Extension{
Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) {
if args.Data.Email == "" {
return nil, errors.New("email is required")
}
return next(ctx, args)
},
})Relation Sub-queries & Selections
Customize returned scalar fields and pre-load relations on args.Select:
db.User.Use(user.Extension{
Create: func(ctx context.Context, args *phi.UserCreateArgs, next phi.UserCreateQuery) (*phi.User, error) {
args.Select.Email = true
args.Select.Posts = post.Query().Where(post.Published.EQ(true))
return next(ctx, args)
},
})2. Bulk Insert (`CreateMany`)
*phi.UserCreateManyArgs exposes:
Data []*phi.UserCreate: Slice of record inputs.ConflictTarget&ConflictAction: Upsert rules for bulk conflicts.
Batch Input Mutation & Validation
Iterate over args.Data to validate or modify entries in-place:
db.User.Use(user.Extension{
CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) {
for _, record := range args.Data {
record.Email = strings.ToLower(record.Email)
}
return next(ctx, args)
},
})Appending Records
Use args.AppendData(...) to dynamically inject extra records into the batch:
db.User.Use(user.Extension{
CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) {
args.AppendData(
db.User.Create().SetEmail("audit1@example.com").SetPhoneNum("+1001"),
db.User.Create().SetEmail("audit2@example.com").SetPhoneNum("+1002"),
)
return next(ctx, args)
},
})3. Bulk Insert and Return (`CreateManyAndReturn`)
*phi.UserCreateManyAndReturnArgs combines batch data manipulation with relation selection:
db.User.Use(user.Extension{
CreateManyAndReturn: func(ctx context.Context, args *phi.UserCreateManyAndReturnArgs, next phi.UserCreateManyAndReturnQuery) ([]*phi.User, error) {
// Enforce default role across all batch entries
defaultRole := phi.UserRole_STUDENT
for _, record := range args.Data {
if record.Role == nil {
record.Role = &defaultRole
}
}
// Return nested posts for all created users
args.Select.Posts = post.Query()
return next(ctx, args)
},
})4. Upsert & Conflict Interception
Intercept conflict resolution strategies (OnConflict) across single and bulk creation:
db.User.Use(user.Extension{
CreateMany: func(ctx context.Context, args *phi.UserCreateManyArgs, next phi.UserCreateManyQuery) (int64, error) {
if args.ConflictAction != nil && args.ConflictAction.IsUpdateNewValues() {
// Override conflict action with a custom update builder
args.ConflictAction = user.ConflictUpdate(func(u *phi.UserUpsert) {
u.Role.Set(phi.UserRole_STUDENT)
u.LoginCount.Increment(1)
})
}
return next(ctx, args)
},
})