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:
Rezon Philip 2025-11-29 20:52:17 +05:30
parent fe50efd3ab
commit b99866db54
3 changed files with 316 additions and 2 deletions

View File

@ -1,7 +1,313 @@
package main 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() { 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 its 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 Echos 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 cant 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),
},
})
}
} }

View File

@ -2,9 +2,12 @@ module github.com/creativenoz/aurganize-v62/backend
go 1.25.2 go 1.25.2
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect

View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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= 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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 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/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 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 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/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 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=