Hello,
I’ve read a lot of articles (usually to simple to answer to what I’m wondering), read some books, pratice a lot but I have still questions about how to architecture a web application or at least, for a complex application, I’m finding myself writing functions that do too much things and I’m not able to refactor in a good way.
For my web application, I’m always using:
- sqlc to generate the SQL boilerplate code
- chi router or alternative
- templ to build HTML with Go
So here is my project layout (approximatively):
cmd/ # main app here
internal/
database/ # code to initialise database, create connections...
db/migrations/*sql # sql code migrations (for dbmate or alternatives)
html
handlers/ # all my handlers
views/ # all the templ code to generate html
middlewares/
repository/ # code generated by sqlc
...
pkg/
config # config package to handle the configuration of the app
user # to handle users for examples
token # to handle the auth of users for example
subscription # to handle the subscriptions of user
...
queries/ # contains all requests that will be used by sqlc to generate the boilerplate code
Packages user, token, subscription handles generally the basic operations (create, fetch, delete, update). I’m writing all of them in the same way, it’s a service layer.
For example, for my user package, I have this:
package user
...
type Service struct {
queries *repository.Queries
}
func NewService(db repository.DBTX) *Service {
queries := repository.New(db)
return &Service{
queries: queries,
}
}
func (s *Service) CreateNewUser(...)
// GetUserFromEmail returns the user with the given email
func (s *Service) GetUserFromEmail(ctx context.Context, email string) (repository.ClientsUser, error) {
return s.queries.GetUserWithItsEmail(ctx, email)
}
...
Now for my handlers package, as they need to use all the services, the constructor can be huge. In my actual project, it’s something like this:
package handlers
func NewController(cfg config.ConfigApp, userSvc *user.Service,
tokenSvc *token.Service, fileSvc *file.Service,
countrySvc *country.Service, redisClient *redis.Client,
store storage.Storage, tSvc *turnstile.TurnstileSvc,
mailSvc mailservice.SendMailer,
stripeSvc *stripesvc.StripeSvc,
subscriptionSvc *subscription.Service,
productSvc *product.Service) *Controller {
return &Controller{
cfg: cfg,
userSvc: userSvc,
tokenSvc: tokenSvc,
fileSvc: fileSvc,
countrySvc: countrySvc,
storage: store,
turnstileSvc: tSvc,
mailSvc: mailSvc,
stripeSvc: stripeSvc,
subscriptionSvc: subscriptionSvc,
productSvc: productSvc,
}
}
...
Does it seem ok to have a such controller ? (It’s very anoying to write unit tests with that, even if I can pass nil sometimes)
And now is my main problem. I have business logic in my handler. For example, for a webapp that allow users to upload files. The business logic could be different if the user has a subscription or not, and be different in function of the type of the subscription.
I find myself writing the upload handler like that:
- check JWT token is valid with tokensvc ( this part should be in a middleware, ok with that)
- identify user from the email in the token with usersvc
- retrieve the size of the request to check later if the subscription of the user allow to upload it
- get the active product for the user
- calculating the uploaded files to check if the user can upload the actual file
- I have some business logic based on subscriptions
- and here I handle the upload of the file
This handler use:
- token service
- subscription service
- product service
- user service
I’m wondering if the user service should not use itself subscriptions service, product service for example ? My main business logic is around that.
If you had the patience to read until here, thank you.