E
E
Eugene2020-03-20 03:21:45
go
Eugene, 2020-03-20 03:21:45

How to structure http server?

I'm using the gorilla/mux package and I've run into the problem that I have to put all the logic in one file. No matter how much I searched, I can’t find an example of how to properly separate an application.

I'll give you an example.
server.go file. It describes the server type and contains the function for creating a new server and all handlers, middlewares.

type server struct {
  router       *mux.Router
  logger       *logrus.Logger
  store        store.Store
  sessionStore sessions.Store
}

func newServer(store store.Store, sessionStore sessions.Store) *server {
  s := &server{
    router:       mux.NewRouter(),
    logger:       logrus.New(),
    store:        store,
    sessionStore: sessionStore,
  }

  s.configureRouter()

  return s
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  s.router.ServeHTTP(w, r)
}


Router configuration.
func (s *server) configureRouter() {
  s.router.Use(s.setRequestID)
  s.router.Use(s.logRequest)
  s.router.Use(handlers.CORS(handlers.AllowedOrigins([]string{"*"})))
  s.router.HandleFunc("/users", s.handleUsersCreate()).Methods("POST")
  s.router.HandleFunc("/sessions", s.handleSessionCreate()).Methods("POST")

  private := s.router.PathPrefix("/private").Subrouter()
  private.Use(s.authenticateUser)
  private.HandleFunc("/whoami", s.handleWhoami()).Methods("GET")
}


Handler example.
func (s *server) handleWhoami() http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    s.respond(w, r, http.StatusOK, r.Context().Value(ctxKeyUser).(*model.User))
  }
}


I can't move the processing to a separate file, because it seems to me that this will lead to cyclic imports, and writing in one file is not very convenient. I want to separate the server code from the business logic.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
W
WinPooh32, 2020-03-20
@WinPooh32

I use this approach:
1) Handlers are only concerned with processing input parameters from the client, for example, extracting parameters, displaying errors to the user;
2) Business logic is placed in a separate package or class;
3) Requests to external storages are also in a separate package;
4) Passing database or storage connections only through context using middleware.
Project structure example:
| + api
| - - router.go
| + models
| - - modelA.go
| - - modelB.go
| + app
| - - taskA.go
| - - taskB.go
| + database
| - - requests.go
| main.go
main.go:

package main
import "api"
func main(){
  ...
  r := api.NewRouter(...)
  http.ListenAndServe(...)
}

api/router.go:
package api
import (
  "app"
  "models"
)
func NewRouter(...) ...{
  // Инициализация роутера, регистрация обработчиков и т.д.
  ...
  return r
}

func handleEndpointA(w http.ResponseWriter, r *http.Request){
  // Извлечение, валидация параметров.
  // Извлечение соединений БД и друхих хранилищ из контекста.
  // Обработка ошибок.
  ...
  result, err := app.DoTaskA(ctx, db, paramA, paramB)
  ...
  render(w, result)
}

func handleEndpointB(w http.ResponseWriter, r *http.Request){
  ...
  result, err := app.DoTaskB(ctx, db, paramC)
  ...
  render(w, result)
}

app/taskA.go:
package app
import(
    "database"
    "models"
)
func DoTaskA(ctx, db, paramA, paramB) (result, err){
  // Какие-то манипуляции с параметрами.
  ...
  // Запрос в бд.
  modelA, err := database.RequestA(ctx, db, param)
  ...
  return result, err
}

database/requests.go:
package database
import (
  "models"
)
func RequestA(ctx, db, param) (result, err) {
  // Не забываем поставить таймаут на запрос к БД.
  ctx, cancel = context.WithTimeout(ctx, defaultRequestTimeout)
  defer cancel()
  ...
  result, err := db.DoRequest(ctx, ...)
  ...
  return result, err
}

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question