Skip to content

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 ​

go
// 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:

go
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:

go
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:

json
{
    "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:

go
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 ​

Released under the MIT License.