saving the work
							parent
							
								
									ccbd749d80
								
							
						
					
					
						commit
						f8b93af5c9
					
				@ -1,3 +1,3 @@
 | 
				
			|||||||
# cookiecutter-golang-server
 | 
					# cookiecutter-golang-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
My cookiecutter project layout for a golang server application
 | 
					My cookiecutter project layout for a golang server application. This is delivered as a git template.
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					module git.runcible.io/androiddrew/cookiecutter-golang-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.23.3
 | 
				
			||||||
@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Config struct {
 | 
				
			||||||
 | 
						msg string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *Config) GetMessage() string {
 | 
				
			||||||
 | 
						if c.msg == "" {
 | 
				
			||||||
 | 
							return "dirp"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return c.msg
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleSomething handles one of those web requests
 | 
				
			||||||
 | 
					// that you hear so much about.
 | 
				
			||||||
 | 
					func handleSomething(logger *slog.Logger, config *Config) http.Handler {
 | 
				
			||||||
 | 
						// provides a closure environment for the function
 | 
				
			||||||
 | 
						// thing := prepareThing()
 | 
				
			||||||
 | 
						msg := config.GetMessage()
 | 
				
			||||||
 | 
						return http.HandlerFunc(
 | 
				
			||||||
 | 
							func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
								// use thing to handle request
 | 
				
			||||||
 | 
								logger.Info("Handle something called", "msg", msg)
 | 
				
			||||||
 | 
								w.Write([]byte("handledSomething..."))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// handleHealthzPlease returns a healthyu en
 | 
				
			||||||
 | 
					func handleHealthzPlease(logger *slog.Logger) http.Handler {
 | 
				
			||||||
 | 
						response := map[string]string{"status": "healthy"}
 | 
				
			||||||
 | 
						return http.HandlerFunc(
 | 
				
			||||||
 | 
							func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
								logger.Debug("Health endpoint called", "method", r.Method, "url", r.URL.Path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if err := encode(w, r, http.StatusOK, response); err != nil {
 | 
				
			||||||
 | 
									serverError(logger, w, r, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseLogLevel(levelStr string) slog.Level {
 | 
				
			||||||
 | 
						switch strings.ToUpper(levelStr) {
 | 
				
			||||||
 | 
						case "DEBUG":
 | 
				
			||||||
 | 
							return slog.LevelDebug
 | 
				
			||||||
 | 
						case "INFO":
 | 
				
			||||||
 | 
							return slog.LevelInfo
 | 
				
			||||||
 | 
						case "WARN":
 | 
				
			||||||
 | 
							return slog.LevelWarn
 | 
				
			||||||
 | 
						case "ERROR":
 | 
				
			||||||
 | 
							return slog.LevelError
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return slog.LevelInfo // Default level
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// InitLoggging initializes global structured logging for the entire application
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Stderr is used for diagnostics and logging. Stdout is used for program
 | 
				
			||||||
 | 
					// output. Stderr should be used since it will have greater likely hood of being
 | 
				
			||||||
 | 
					// seen if a programs output is being redirected.
 | 
				
			||||||
 | 
					func InitLogging(level string, w io.Writer) *slog.Logger {
 | 
				
			||||||
 | 
						parsedLogLevel := parseLogLevel(level)
 | 
				
			||||||
 | 
						loggerHandler := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: true})
 | 
				
			||||||
 | 
						logger := slog.New(loggerHandler)
 | 
				
			||||||
 | 
						slog.SetDefault(logger)
 | 
				
			||||||
 | 
						return logger
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ExampleModel struct {
 | 
				
			||||||
 | 
						ID        int
 | 
				
			||||||
 | 
						CreatedAt time.Time
 | 
				
			||||||
 | 
						UpdatedAt time.Time
 | 
				
			||||||
 | 
						Title     sql.NullString
 | 
				
			||||||
 | 
						Content   sql.NullString
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ExampleModelService struct {
 | 
				
			||||||
 | 
						DB *sql.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *ExampleModelService) CreateExample(ctx context.Context, example *ExampleModel) (int, error) {
 | 
				
			||||||
 | 
						tx, err := s.DB.BeginTx(ctx, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer tx.Rollback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						id, err := createExample(ctx, tx, example)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return id, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return id, tx.Commit()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createExample(ctx context.Context, tx *sql.Tx, example *ExampleModel) (int, error) {
 | 
				
			||||||
 | 
						// assign current user to model
 | 
				
			||||||
 | 
						// userID := UserIDFromContext(ctx)
 | 
				
			||||||
 | 
						// if userID == 0 {
 | 
				
			||||||
 | 
						// 	return 0,  wtf.Errorf(wtf.EUNAUTHORIZED, "You must be logged in to create a dial.")
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
						// example.UserID = wtf.UserIDFromContext(ctx)
 | 
				
			||||||
 | 
						stmt := "INSERT INTO example (title, content) VALUES (?, ?)"
 | 
				
			||||||
 | 
						result, err := tx.ExecContext(ctx, stmt, example.Title, example.Content)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						id, err := result.LastInsertId()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return int(id), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addRoutes(mux *http.ServeMux,
 | 
				
			||||||
 | 
						logger *slog.Logger,
 | 
				
			||||||
 | 
						config *Config,
 | 
				
			||||||
 | 
						//tenantStore *TenantStore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					) http.Handler {
 | 
				
			||||||
 | 
						// mux.Handle("/api/v1/", handleTenantsGet(logger, tenantsStore))
 | 
				
			||||||
 | 
						// mux.Handle("/oauth2/", handleOAuth2Proxy(logger, authProxy))
 | 
				
			||||||
 | 
						mux.Handle("/something", handleSomething(logger, config))
 | 
				
			||||||
 | 
						mux.Handle("/healthz", handleHealthzPlease(logger))
 | 
				
			||||||
 | 
						mux.Handle("/", http.NotFoundHandler())
 | 
				
			||||||
 | 
						return mux
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewServer is responsible for all the top-level HTTP stuff that
 | 
				
			||||||
 | 
					// applies to all endpoints, like CORS, auth middleware, and logging.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// in tests `nil` can be passed to components that will not be strictly
 | 
				
			||||||
 | 
					// used.
 | 
				
			||||||
 | 
					func NewServer(logger *slog.Logger,
 | 
				
			||||||
 | 
						config *Config,
 | 
				
			||||||
 | 
						// commentStore *commentStore
 | 
				
			||||||
 | 
						// anotherStore *anotherStore
 | 
				
			||||||
 | 
					) http.Handler {
 | 
				
			||||||
 | 
						mux := http.NewServeMux()
 | 
				
			||||||
 | 
						addRoutes(
 | 
				
			||||||
 | 
							mux,
 | 
				
			||||||
 | 
							logger,
 | 
				
			||||||
 | 
							config,
 | 
				
			||||||
 | 
							// commentStore,
 | 
				
			||||||
 | 
							// anotherStore,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						var handler http.Handler = mux
 | 
				
			||||||
 | 
						// Add Middleware
 | 
				
			||||||
 | 
						// handler = someMiddleware(handler)
 | 
				
			||||||
 | 
						// handler = someMiddleware2(handler)
 | 
				
			||||||
 | 
						// handler = someMiddleware3(handler)
 | 
				
			||||||
 | 
						return handler
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,110 @@
 | 
				
			|||||||
 | 
					package internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"runtime/debug"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// encode writes a JSON-encoded response to the provided http.ResponseWriter.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Parameters:
 | 
				
			||||||
 | 
					// - w: The http.ResponseWriter where the response will be written.
 | 
				
			||||||
 | 
					// - r: The *http.Request associated with the response. (Unused in this function but could be relevant for context.)
 | 
				
			||||||
 | 
					// - status: The HTTP status code to send with the response.
 | 
				
			||||||
 | 
					// - v: The value to encode and send as the JSON response. Can be of any type.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Returns:
 | 
				
			||||||
 | 
					// - An error if the JSON encoding or writing to the ResponseWriter fails, otherwise nil.
 | 
				
			||||||
 | 
					func encode[T any](w http.ResponseWriter, r *http.Request, status int, v T) error {
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
						w.WriteHeader(status)
 | 
				
			||||||
 | 
						if err := json.NewEncoder(w).Encode(v); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("encode json: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// decode reads and decodes a JSON-encoded request body into a value of the specified type.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Parameters:
 | 
				
			||||||
 | 
					// - r: The *http.Request containing the JSON-encoded body to decode.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Returns:
 | 
				
			||||||
 | 
					// - The decoded value of type T.
 | 
				
			||||||
 | 
					// - An error if decoding fails, or if the body contains invalid JSON.
 | 
				
			||||||
 | 
					func decode[T any](r *http.Request) (T, error) {
 | 
				
			||||||
 | 
						var v T
 | 
				
			||||||
 | 
						if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
 | 
				
			||||||
 | 
							return v, fmt.Errorf("decode json: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// serverError logs an internal server error and sends a 500 Internal Server Error
 | 
				
			||||||
 | 
					// response to the client.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Usage:
 | 
				
			||||||
 | 
					// This function is intended for use when handling unexpected server-side errors.
 | 
				
			||||||
 | 
					// It logs the error details along with the HTTP request method, URI, and a stack
 | 
				
			||||||
 | 
					// trace for debugging purposes. After logging, it sends a standardized 500 Internal
 | 
				
			||||||
 | 
					// Server Error response to the client.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Parameters:
 | 
				
			||||||
 | 
					// - logger: The *slog.Logger instance used for logging error details.
 | 
				
			||||||
 | 
					// - w: The http.ResponseWriter to send the response to.
 | 
				
			||||||
 | 
					// - r: The *http.Request that triggered the error. Used to extract method and URI.
 | 
				
			||||||
 | 
					// - err: The error instance to log, providing context about the issue.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Example:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	func handler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					//	    err := someOperation()
 | 
				
			||||||
 | 
					//	    if err != nil {
 | 
				
			||||||
 | 
					//	        serverError(logger, w, r, err)
 | 
				
			||||||
 | 
					//	        return
 | 
				
			||||||
 | 
					//	    }
 | 
				
			||||||
 | 
					//	    // Handle request normally
 | 
				
			||||||
 | 
					//	}
 | 
				
			||||||
 | 
					func serverError(logger *slog.Logger, w http.ResponseWriter, r *http.Request, err error) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							method = r.Method
 | 
				
			||||||
 | 
							uri    = r.URL.RequestURI()
 | 
				
			||||||
 | 
							// Use debug.Stack() to get the stack trace. This returns a byte slice, which
 | 
				
			||||||
 | 
							// we need to convert to a string so that it's readable in the log entry.
 | 
				
			||||||
 | 
							trace = string(debug.Stack())
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace)
 | 
				
			||||||
 | 
						http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// clientError sends an HTTP error response to the client with the specified
 | 
				
			||||||
 | 
					// status code and its corresponding description.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Usage:
 | 
				
			||||||
 | 
					// This function is useful for handling HTTP errors in a consistent manner.
 | 
				
			||||||
 | 
					// It sends the appropriate HTTP status text as the response body, along with
 | 
				
			||||||
 | 
					// the given status code as the response status. This is typically used in web
 | 
				
			||||||
 | 
					// applications to return standardized error messages for various client-side
 | 
				
			||||||
 | 
					// errors.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Parameters:
 | 
				
			||||||
 | 
					//   - w: The http.ResponseWriter to send the response to.
 | 
				
			||||||
 | 
					//   - status: The HTTP status code to return. It should be a valid HTTP status
 | 
				
			||||||
 | 
					//     code as defined in the http package.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Example:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	func handler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					//	    if !isAuthorized(r) {
 | 
				
			||||||
 | 
					//	        clientError(w, http.StatusForbidden) // Responds with "403 Forbidden"
 | 
				
			||||||
 | 
					//	        return
 | 
				
			||||||
 | 
					//	    }
 | 
				
			||||||
 | 
					//	    // Handle request normally
 | 
				
			||||||
 | 
					//	}
 | 
				
			||||||
 | 
					func clientError(w http.ResponseWriter, status int) {
 | 
				
			||||||
 | 
						http.Error(w, http.StatusText(status), status)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
					Loading…
					
					
				
		Reference in New Issue