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