feat(echo server): Define backend api server using echo framework
API server (backend/cmd/api): - we load configuration - init logger - create echo server instance - we define the middleware pipeline --- recover --- request_id --- logger_format --- cors (ross-Origin Resource Sharing) --- security_headers --- gzip_compresssion - route_mapping - star_server - graceful_shutdown add-ons: - healthCheckHandler() - customHTTPErrorHandler() This commit establishes the foundation for all future development. Story: E1-002 - Backend Project Initialization (main echo server)
This commit is contained in:
parent
fe50efd3ab
commit
b99866db54
|
|
@ -1,7 +1,313 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creativenoz/aurganize-v62/backend/internal/config"
|
||||
"github.com/creativenoz/aurganize-v62/backend/pkg/logger"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Aurganize")
|
||||
// =========================================================================
|
||||
// Loading Configuration
|
||||
// =========================================================================
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
// we are not using logger here, since we need config information to set the log level
|
||||
fmt.Fprintf(os.Stderr, "Failed to load configurations : %v\n", err)
|
||||
// hence when config load fails we exit application, cause have not point in continuing further
|
||||
os.Exit(1)
|
||||
}
|
||||
// =========================================================================
|
||||
// Initializing Logger
|
||||
// =========================================================================
|
||||
logger.Init(cfg.Server.Environment)
|
||||
log.Info().
|
||||
Str("Version", "0.6.2").
|
||||
Str("environment", cfg.Server.Environment).
|
||||
Msg("Starting Aurganize v6.2 API server")
|
||||
|
||||
// =========================================================================
|
||||
// Create Echo Instance
|
||||
// =========================================================================
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
|
||||
e.HTTPErrorHandler = customHTTPErrorHandler // we are using a custom error handler
|
||||
|
||||
e.Server.ReadTimeout = cfg.Server.ReadTimeout
|
||||
e.Server.WriteTimeout = cfg.Server.WriteTimeout
|
||||
|
||||
log.Info().Msg("Echo server instance created")
|
||||
// =========================================================================
|
||||
// Middleware Pipeline
|
||||
// =========================================================================
|
||||
|
||||
// Setting safe recover middleware
|
||||
e.Use(middleware.Recover())
|
||||
// Middleware catches panic
|
||||
// Returns 500 Internal Server Error
|
||||
// Server keeps running
|
||||
// -------------------------------------------------------------------------
|
||||
// Setting request ID middleware
|
||||
e.Use(middleware.RequestID())
|
||||
// Trace request through entire system
|
||||
// Link frontend error to backend logs
|
||||
// This adds a header : X-Request-ID: abc-123-def-456
|
||||
// ------------------------------------------------------------------------
|
||||
// Setting Logger format
|
||||
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: `{"time":"${time_rfc3339}","method":"${method}","uri":"${uri}",` +
|
||||
`"status":${status},"latency_ms":${latency_ms},"error":"${error}"}` + "\n",
|
||||
Output: log.Logger,
|
||||
}))
|
||||
// We are setting a custom log format, which is consisten with our logger format
|
||||
// {
|
||||
// "time": "2025-11-26T10:30:45Z",
|
||||
// "method": "POST",
|
||||
// "uri": "/api/v1/login",
|
||||
// "status": 200,
|
||||
// "latency_ms": 45,
|
||||
// "error": ""
|
||||
// }
|
||||
// -----------------------------------------------------------------------
|
||||
// Setting CORS (Cross-Origin Resource Sharing) middleware
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{
|
||||
"http://localhost:5173", // (Development) Svelte dev server : this is the port suggested to be used with front-end
|
||||
"http://localhost:3000", // (Developement) Alternative dev port : this is an alternative port kept aside
|
||||
"https://app.aurganize.com", // (Production) Production frontend : we can use this subdomain itself for front-end
|
||||
},
|
||||
AllowMethods: []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodDelete,
|
||||
http.MethodPatch,
|
||||
http.MethodOptions,
|
||||
},
|
||||
AllowHeaders: []string{
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Accept",
|
||||
"Authorization",
|
||||
"X-Request-ID",
|
||||
},
|
||||
|
||||
AllowCredentials: true, // Not sure about why are using this option
|
||||
|
||||
MaxAge: 3600, // 1 hour in seconds
|
||||
}))
|
||||
// Prevents malicious sites from calling your API
|
||||
// ----------------------------------------------------------------------
|
||||
// Setting Security Headers middleware
|
||||
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
|
||||
XSSProtection: "1; mode=block",
|
||||
ContentTypeNosniff: "nosniff",
|
||||
XFrameOptions: "SAMEORIGIN",
|
||||
HSTSMaxAge: 31536000,
|
||||
HSTSExcludeSubdomains: false,
|
||||
ContentSecurityPolicy: "default-src 'self'",
|
||||
}))
|
||||
// X-XSS-Protection:
|
||||
// - Blocks cross-site scripting attacks
|
||||
// - Browser detects XSS and blocks page
|
||||
|
||||
// X-Content-Type-Options: nosniff:
|
||||
// - Prevents MIME-type sniffing
|
||||
// - Browser trusts Content-Type header
|
||||
// - Prevents executing scripts as HTML
|
||||
|
||||
// X-Frame-Options: SAMEORIGIN:
|
||||
// - Prevents clickjacking
|
||||
// - Page can't be embedded in iframe (except same origin)
|
||||
// - Protects against UI redress attacks
|
||||
|
||||
// Strict-Transport-Security (HSTS):
|
||||
// - Forces HTTPS for 1 year
|
||||
// - Prevents downgrade attacks
|
||||
// - Can't be disabled by user
|
||||
|
||||
// Content-Security-Policy:
|
||||
// - Only load resources from same origin
|
||||
// - Prevents loading malicious scripts
|
||||
// - Additional layer of XSS protection
|
||||
// -------------------------------------------------------------------
|
||||
// Setting Gzip compression middleware
|
||||
e.Use(middleware.Gzip())
|
||||
|
||||
// TODO : Rate Limiting middleware (planning to use redis for custom rate limiter)
|
||||
|
||||
log.Info().Msg("Middleware configured")
|
||||
|
||||
// =========================================================================
|
||||
// Middleware Pipeline
|
||||
// =========================================================================
|
||||
|
||||
e.GET("/health", healthCheckHandler(cfg)) // (Public - health check)
|
||||
api := e.Group("/api/v6.2")
|
||||
|
||||
api.GET("/ping", func(c echo.Context) error { // (Public - connectivity test)
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"message": "pika pikaaa",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"version": "0.6.2",
|
||||
})
|
||||
})
|
||||
|
||||
log.Info().Msg("Routes configured")
|
||||
// =========================================================================
|
||||
// Start Server in a new thread
|
||||
// =========================================================================
|
||||
|
||||
serverAddr := fmt.Sprintf(":%s", cfg.Server.Port)
|
||||
|
||||
go func() {
|
||||
log.Info().
|
||||
Str("address", serverAddr).
|
||||
Str("environment", cfg.Server.Environment).
|
||||
Msg("Server starting")
|
||||
|
||||
if err := e.Start(serverAddr); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to start server")
|
||||
}
|
||||
}()
|
||||
// =========================================================================
|
||||
// Shutdown Logic
|
||||
// =========================================================================
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
// Creates a channel named quit that can carry values of type os.Signal.
|
||||
// The 1 means it’s a buffered channel with capacity 1 → it can hold one signal without blocking.
|
||||
// This channel will be used to receive OS signals like Ctrl+C or kill.
|
||||
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
// Whenever the process receives any of these signals, send them into the quit channel.”
|
||||
// os.Interrupt → typically the signal sent when you press Ctrl+C in the terminal.
|
||||
// syscall.SIGTERM → the “please terminate” signal, used by process managers / Docker / Kubernetes,
|
||||
|
||||
<-quit // Blocks until we get a signal in this channel
|
||||
// This is a receive operation on the channel.
|
||||
// The code blocks here and does nothing until:
|
||||
// the OS sends os.Interrupt or SIGTERM,
|
||||
// which signal.Notify pushes into quit.
|
||||
// When a signal arrives, <-quit unblocks and the program continues.
|
||||
|
||||
log.Info().Msg("Shutting down server gracefully..")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
// Creates a context with timeout of 10 seconds.
|
||||
// This context is passed to e.Shutdown(ctx)
|
||||
// Echo will have at most 10 seconds to shut down gracefully.
|
||||
// After 10 seconds, the context is cancelled, and shutdown will be forced.
|
||||
|
||||
if err := e.Shutdown(ctx); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Server forced to shutdown, graceful shutdown failed")
|
||||
}
|
||||
// Calls Echo’s Shutdown method with your timeout context.
|
||||
// e.Shutdown(ctx):
|
||||
// -- stops accepting new requests,
|
||||
// -- waits for in-flight requests to finish,
|
||||
// -- closes the server gracefully (within the timeout).
|
||||
// If something goes wrong (e.g., it can’t shut down in time), err is non-nil:
|
||||
// -- logs an error saying graceful shutdown failed and it had to force close.
|
||||
|
||||
log.Info().Msg("API Server exited")
|
||||
|
||||
}
|
||||
|
||||
// HealthCheck Handler function
|
||||
// This endpoint is to be used by:
|
||||
// - Load balancers to determine if instance is healthy
|
||||
// - Kubernetes for liveness and readiness probes
|
||||
// - Monitoring systems for uptime checks
|
||||
func healthCheckHandler(c *config.Config) echo.HandlerFunc {
|
||||
return func(e echo.Context) error {
|
||||
// TODO : we need to add health check for
|
||||
// - Database connection
|
||||
// - Redis connection if we are using it
|
||||
// - NATS connection
|
||||
|
||||
// For now we are just returning OK, since most of the service dependecies are not implemented yet.
|
||||
response := map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"version": "0.6.2",
|
||||
"environment": c.Server.Environment,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"checks": map[string]string{
|
||||
"server": "ok",
|
||||
"database": "not setup",
|
||||
"redis": "not setup",
|
||||
"nats": "not setup",
|
||||
},
|
||||
"uptime": 999999999999, // logic yet to be implemented
|
||||
}
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
|
||||
// CustomHTTPErrorHandler
|
||||
// Used to provide consistent error response
|
||||
// Converts Echo erros to JSON format, that can be parsed at frontend
|
||||
func customHTTPErrorHandler(err error, c echo.Context) {
|
||||
|
||||
// Setting default values
|
||||
code := http.StatusInternalServerError
|
||||
message := "internal server error"
|
||||
|
||||
// Checking if the error is and Echo error
|
||||
// We do this through a type assertion err.(*echo.HTTPError) [ basetype.(type to assert to)]
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
code = he.Code
|
||||
// Then we again check with a type assertion
|
||||
if msg, ok := he.Message.(string); ok {
|
||||
message = msg
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Int("status", code).
|
||||
Str("method", c.Request().Method).
|
||||
Str("path", c.Request().URL.Path).
|
||||
Msg("HTTP error")
|
||||
|
||||
if code >= 500 {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("status", code).
|
||||
Str("method", c.Request().Method).
|
||||
Str("path", c.Request().URL.Path).
|
||||
Msg("HTTP error")
|
||||
}
|
||||
|
||||
// If the response is not already written
|
||||
// Then we don't have to again log
|
||||
if !c.Response().Committed {
|
||||
c.JSON(code, map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"code": code,
|
||||
"message": message,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"path": c.Request().URL.Path,
|
||||
"request_id": c.Response().Header().Get(echo.HeaderXRequestID),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ module github.com/creativenoz/aurganize-v62/backend
|
|||
|
||||
go 1.25.2
|
||||
|
||||
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
|
|
@ -35,6 +37,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
|
|
@ -77,6 +81,7 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
|||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
|
|
|
|||
Loading…
Reference in New Issue