Recipe - CRUD REST API with Validation
Full CRUD REST API using xvalidator.BodyValidator[T] middleware for route-level decode + validate, path constraints, and an in-memory store.
CRUD REST API with Validation
Build a type-safe CRUD API using Kern's binding, xvalidator (based on go-playground/validator), and path constraints.
Struct with validation tags
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=1,max=150"`
Role string `json:"role" validate:"required,oneof=admin viewer editor"`
}
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}In-memory store with thread safety
type UserStore struct {
mu sync.RWMutex
users map[int]*UserResponse
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]*UserResponse),
nextID: 1,
}
}
func (s *UserStore) Create(req CreateUserRequest) UserResponse {
s.mu.Lock()
defer s.mu.Unlock()
u := UserResponse{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
Role: req.Role,
CreatedAt: time.Now(),
}
s.nextID++
s.users[u.ID] = &u
return u
}
func (s *UserStore) Get(id int) (UserResponse, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
if !ok {
return UserResponse{}, false
}
return *u, true
}
func (s *UserStore) List() []UserResponse {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]UserResponse, 0, len(s.users))
for _, u := range s.users {
result = append(result, *u)
}
return result
}
func (s *UserStore) Update(id int, req CreateUserRequest) (UserResponse, bool) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[id]
if !ok {
return UserResponse{}, false
}
u.Name = req.Name
u.Email = req.Email
u.Age = req.Age
u.Role = req.Role
return *u, true
}
func (s *UserStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.users[id]
if !ok {
return false
}
delete(s.users, id)
return true
}Route registration with middleware and path constraints
xvalidator.BodyValidator[T]() decodes and validates the request body in a single middleware step. The validated struct is stored in the request context and retrieved with xvalidator.Validated[T]().
func main() {
store := NewUserStore()
app := kern.New(
kern.WithRecovery(),
kern.WithSlogLogger(slog.New(slog.NewTextHandler(os.Stdout, nil))),
)
api := app.Group("/api/v1")
api.AddConstraints("POST", "/users", kern.Constraints{
Validate: xvalidator.BodyValidator[CreateUserRequest](),
}, func(c *kern.Context) {
req, _ := xvalidator.Validated[CreateUserRequest](c.Context())
user := store.Create(req)
c.Created(user)
})
api.AddConstraints("GET", "/users/{id}", kern.Constraints{
Path: kern.PathConstraints{"id": kern.UintPathConstraint},
}, func(c *kern.Context) {
id, _ := strconv.Atoi(c.Param("id"))
user, ok := store.Get(id)
if !ok {
c.Error(http.StatusNotFound, "user not found")
return
}
c.OK(user)
})
api.GET("/users", func(c *kern.Context) {
c.OK(store.List())
})
api.AddConstraints("PUT", "/users/{id}", kern.Constraints{
Path: kern.PathConstraints{"id": kern.UintPathConstraint},
Validate: xvalidator.BodyValidator[CreateUserRequest](),
}, func(c *kern.Context) {
id, _ := strconv.Atoi(c.Param("id"))
req, _ := xvalidator.Validated[CreateUserRequest](c.Context())
user, ok := store.Update(id, req)
if !ok {
c.Error(http.StatusNotFound, "user not found")
return
}
c.OK(user)
})
api.AddConstraints("DELETE", "/users/{id}", kern.Constraints{
Path: kern.PathConstraints{"id": kern.UintPathConstraint},
}, func(c *kern.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if !store.Delete(id) {
c.Error(http.StatusNotFound, "user not found")
return
}
c.NoContent(http.StatusNoContent)
})
app.Run(":8080")
}Key patterns
- Validation middleware:
xvalidator.BodyValidator[T]()decodes + validates in one step;xvalidator.Validated[T]()retrieves the struct from context - Validation tags:
required,min,max,oneof,emailon struct fields viaxvalidator - Custom messages: override error messages per tag with
xvalidator.Default().SetMessages() - Path constraints:
kern.UintPathConstraintensures type-safe path params - Error responses: Structured
c.Errorandc.JSONErrorfor consistent API errors - Thread-safe store:
sync.RWMutexfor concurrent access
Recipe - Production API Blueprint
End-to-end Kern production API setup with middleware order, timeouts, health endpoints, and graceful shutdown.
Recipe - File Upload & Download Service
Full-file service with multipart upload, type/size validation, streaming download with Range support, and static serving.