Services
The service holds the rules of a feature: what is allowed, what must be true and what happens. Handlers call it, and it calls the repository and other preás' services.
What you'll learn
- What belongs in a service
- How a service rejects a request with the right HTTP status
- How a service uses another preá
A service
// Service holds the rules of a task.
type Service struct {
Repo *Repository `inject:""`
Lists *list.Service `inject:""` // exported by the list preá
}A service is a component like any other. It asks for its repository and, here, for the service of the list preá, which task imports.
Rules
A rule is a decision the service makes before or after it touches the data:
func (s *Service) Create(ctx context.Context, req dtos.CreateRequest) (Task, error) {
req.Title = strings.TrimSpace(req.Title)
if err := checkTitle(req.Title); err != nil {
return Task{}, err
}
if req.ListID != nil {
if err := s.checkList(ctx, *req.ListID); err != nil {
return Task{}, err
}
}
return s.Repo.Create(ctx, req)
}The service cleans the title, checks it, makes sure the list exists, and only then creates the task. The handler doesn't know about any of this; it only hands over the request.
Say no with a status
When a rule fails, return an error made with core.NewError, with the HTTP status that fits:
const maxTitle = 200
func checkTitle(title string) error {
switch {
case title == "":
return core.NewError(http.StatusBadRequest, "title can't be blank")
case utf8.RuneCountInString(title) > maxTitle:
return core.NewError(http.StatusBadRequest, "title is too long: 200 characters at most")
}
return nil
}
// checkList turns a missing list into a clear 422, instead of the
// database's foreign key error.
func (s *Service) checkList(ctx context.Context, id string) error {
_, err := s.Lists.Get(ctx, id)
if errors.Is(err, database.ErrNotFound) {
return core.NewError(http.StatusUnprocessableEntity, "list not found")
}
return err
}The handler passes the error to core.Error, and the client gets that status and message:
{
"text": "title can't be blank",
"code": 2
}Use the net/http constants for the status. http.StatusBadRequest (400) means the request is wrong; http.StatusUnprocessableEntity (422) means it is well-formed but refers to something that doesn't work, like a list that doesn't exist; http.StatusConflict (409) means it clashes with the current state.
Errors from the database
Many database errors already carry a status. database.ErrNotFound replies 404, a duplicate unique value replies 409, and a malformed value replies 400. Return them as they are, unless the client deserves a clearer message, like checkList above, which turns a missing list into "list not found".
Compare them with errors.Is:
if errors.Is(err, database.ErrNotFound) {
// ...
}Pass the context along
Every service method takes a context.Context first and passes it to every call it makes. The request's deadline and cancellation travel with it, and so does the transaction, when there is one.
Next steps
- Errors & responses: the full list of statuses
- Repositories: what the service can ask of the database