Go SDK

Echo Integration

Production-ready integration with the Echo web framework featuring structured logging, request validation, graceful shutdown, and comprehensive error handling.

This example uses github.com/sentdm/sent-dm-go with Echo v4 patterns including middleware, service layer, and clean architecture principles.

Project Structure

sent-echo-app/
├── cmd/api/main.go              # Application entry
├── internal/
│   ├── config/config.go         # Configuration
│   ├── handler/
│   │   ├── health.go            # Health endpoints
│   │   ├── message.go           # Message handlers
│   │   └── webhook.go           # Webhook handlers
│   ├── middleware/
│   │   ├── logger.go            # Request logging
│   │   └── error.go             # Error handling
│   ├── model/request.go         # Request DTOs
│   ├── model/response.go        # Response models
│   ├── service/
│   │   ├── message.go           # Message logic
│   │   └── webhook.go           # Webhook processing
│   └── validator/validator.go   # Validation
├── pkg/sentclient/client.go     # Sent client wrapper
├── docker-compose.yml
├── Dockerfile
├── go.mod
└── .env

Configuration

// internal/config/config.go
package config

import (
	"fmt"
	"time"
	"github.com/kelseyhightower/envconfig"
)

type Config struct {
	Server    ServerConfig
	Sent      SentConfig
	Log       LogConfig
	RateLimit RateLimitConfig
}

type ServerConfig struct {
	Port         string        `envconfig:"PORT" default:"8080"`
	ReadTimeout  time.Duration `envconfig:"SERVER_READ_TIMEOUT" default:"5s"`
	WriteTimeout time.Duration `envconfig:"SERVER_WRITE_TIMEOUT" default:"10s"`
	IdleTimeout  time.Duration `envconfig:"SERVER_IDLE_TIMEOUT" default:"120s"`
	Environment  string        `envconfig:"ENVIRONMENT" default:"development"`
}

type SentConfig struct {
	APIKey        string `envconfig:"SENT_DM_API_KEY" required:"true"`
	WebhookSecret string `envconfig:"SENT_DM_WEBHOOK_SECRET" required:"true"`
}

type LogConfig struct {
	Level  string `envconfig:"LOG_LEVEL" default:"info"`
	Format string `envconfig:"LOG_FORMAT" default:"json"`
}

type RateLimitConfig struct {
	RequestsPerSecond float64 `envconfig:"RATE_LIMIT_RPS" default:"10"`
	BurstSize         int     `envconfig:"RATE_LIMIT_BURST" default:"20"`
}

func Load() (*Config, error) {
	var cfg Config
	if err := envconfig.Process("", &cfg); err != nil {
		return nil, fmt.Errorf("failed to load config: %w", err)
	}
	return &cfg, nil
}

func NewLogger(cfg LogConfig) (*zap.Logger, error) {
	level, _ := zapcore.ParseLevel(cfg.Level)
	encoderConfig := zapcore.EncoderConfig{
		TimeKey: "timestamp", LevelKey: "level", MessageKey: "msg",
		EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder,
	}
	var encoder zapcore.Encoder
	if cfg.Format == "console" {
		encoder = zapcore.NewConsoleEncoder(encoderConfig)
	} else {
		encoder = zapcore.NewJSONEncoder(encoderConfig)
	}
	core := zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), level)
	return zap.New(core, zap.AddCaller()), nil
}

Models

// internal/model/response.go
package model

import "net/http"

type ErrorResponse struct {
	Success   bool      `json:"success"`
	Error     ErrorInfo `json:"error,omitempty"`
	RequestID string    `json:"requestId,omitempty"`
}

type ErrorInfo struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Details string `json:"details,omitempty"`
}

type SuccessResponse[T any] struct {
	Success bool `json:"success"`
	Data    T    `json:"data,omitempty"`
}

func NewSuccessResponse[T any](data T) SuccessResponse[T] {
	return SuccessResponse[T]{Success: true, Data: data}
}

func NewErrorResponse(code, message string) ErrorResponse {
	return ErrorResponse{Success: false, Error: ErrorInfo{Code: code, Message: message}}
}

type HTTPError struct {
	Code       int
	ErrorCode  string
	Message    string
	InnerError error
}

func (e *HTTPError) Error() string { return e.Message }
func (e *HTTPError) Unwrap() error { return e.InnerError }

var (
	ErrBadRequest   = &HTTPError{Code: http.StatusBadRequest, ErrorCode: "BAD_REQUEST", Message: "Invalid request"}
	ErrUnauthorized = &HTTPError{Code: http.StatusUnauthorized, ErrorCode: "UNAUTHORIZED", Message: "Authentication required"}
	ErrValidation   = &HTTPError{Code: http.StatusUnprocessableEntity, ErrorCode: "VALIDATION_ERROR", Message: "Validation failed"}
)

func BindValidationError(err error) *HTTPError {
	return &HTTPError{Code: http.StatusBadRequest, ErrorCode: "VALIDATION_ERROR", Message: "Request validation failed", Details: err.Error(), InnerError: err}
}

// internal/model/request.go
type SendMessageRequest struct {
	To       []string        `json:"to" validate:"required,min=1,dive,e164"`
	Template TemplateRequest `json:"template" validate:"required"`
	Channels []string        `json:"channels,omitempty" validate:"omitempty,dive,oneof=sms whatsapp email telegram"`
}

type TemplateRequest struct {
	ID         string            `json:"id,omitempty" validate:"omitempty,uuid"`
	Name       string            `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
	Parameters map[string]string `json:"parameters,omitempty"`
}

type WelcomeMessageRequest struct {
	PhoneNumber string `json:"phoneNumber" validate:"required,e164"`
	Name        string `json:"name,omitempty" validate:"omitempty,max=100"`
}

Sent Client

// pkg/sentclient/client.go
package sentclient

import (
	"context"
	"fmt"
	"time"
	"github.com/sentdm/sent-dm-go"
	"github.com/sentdm/sent-dm-go/option"
	"go.uber.org/zap"
)

type Client struct {
	client *sentdm.Client
	logger *zap.Logger
}

func New(apiKey string, logger *zap.Logger) *Client {
	return &Client{
		client: sentdm.NewClient(option.WithAPIKey(apiKey)),
		logger: logger.Named("sent-client"),
	}
}

type SendMessageResult struct {
	MessageID string
	Status    string
	Channel   string
}

func (c *Client) SendMessage(ctx context.Context, to []string, template sentdm.MessageSendParamsTemplate, channels []string) (*SendMessageResult, error) {
	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()
	params := sentdm.MessageSendParams{To: to, Template: template}
	if len(channels) > 0 { params.Channels = sentdm.StringSlice(channels) }

	c.logger.Debug("sending message", zap.Strings("to", to), zap.Strings("channels", channels))
	response, err := c.client.Messages.Send(ctx, params)
	if err != nil {
		c.logger.Error("failed to send message", zap.Error(err))
		return nil, fmt.Errorf("failed to send message: %w", err)
	}
	if len(response.Data.Messages) == 0 { return nil, fmt.Errorf("no messages in response") }

	msg := response.Data.Messages[0]
	return &SendMessageResult{MessageID: msg.ID, Status: msg.Status, Channel: msg.Channel}, nil
}

func (c *Client) ParseEvent(body string) (*sentdm.WebhookEvent, error) {
	return c.client.Webhooks.ParseEvent(body)
}

func (c *Client) VerifySignature(body, signature, secret string) bool {
	return c.client.Webhooks.VerifySignature(body, signature, secret)
}

Middleware

// internal/middleware/logger.go
package middleware

import (
	"time"
	"github.com/labstack/echo/v4"
	"go.uber.org/zap"
)

func Logger(logger *zap.Logger) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			start := time.Now()
			err := next(c)
			logger.Info("request",
				zap.String("method", c.Request().Method),
				zap.String("path", c.Request().URL.Path),
				zap.Int("status", c.Response().Status),
				zap.Duration("latency", time.Since(start)),
			)
			return err
		}
	}
}

// internal/middleware/error.go
package middleware

import (
	"errors"
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
	"go.uber.org/zap"
)

func ErrorHandler(logger *zap.Logger) echo.HTTPErrorHandler {
	return func(err error, c echo.Context) {
		if c.Response().Committed { return }
		requestID := c.Response().Header().Get(echo.HeaderXRequestID)

		var httpErr *model.HTTPError
		if errors.As(err, &httpErr) {
			c.JSON(httpErr.Code, model.ErrorResponse{Success: false, RequestID: requestID, Error: model.ErrorInfo{Code: httpErr.ErrorCode, Message: httpErr.Message, Details: httpErr.Details}})
			return
		}

		var echoErr *echo.HTTPError
		if errors.As(err, &echoErr) {
			c.JSON(echoErr.Code, model.NewErrorResponse("ERROR", echoErr.Error()))
			return
		}

		logger.Error("unexpected error", zap.Error(err), zap.String("requestId", requestID))
		c.JSON(http.StatusInternalServerError, model.NewErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
	}
}

Service Layer

// internal/service/message.go
package service

import (
	"context"
	"fmt"
	"github.com/sentdm/sent-dm-go"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
	"github.com/sentdm/sent-dm-go/sent-echo-app/pkg/sentclient"
	"go.uber.org/zap"
)

type MessageService struct {
	sentClient *sentclient.Client
	logger     *zap.Logger
}

func NewMessageService(sentClient *sentclient.Client, logger *zap.Logger) *MessageService {
	return &MessageService{sentClient: sentClient, logger: logger.Named("message-service")}
}

type SendMessageResult struct {
	MessageID string `json:"messageId"`
	Status    string `json:"status"`
	Channel   string `json:"channel,omitempty"`
}

func (s *MessageService) SendMessage(ctx context.Context, req *model.SendMessageRequest) (*SendMessageResult, error) {
	template := sentdm.MessageSendParamsTemplate{ID: sentdm.String(req.Template.ID), Name: sentdm.String(req.Template.Name), Parameters: req.Template.Parameters}
	result, err := s.sentClient.SendMessage(ctx, req.To, template, req.Channels)
	if err != nil {
		return nil, fmt.Errorf("failed to send message: %w", err)
	}
	return &SendMessageResult{MessageID: result.MessageID, Status: result.Status, Channel: result.Channel}, nil
}

func (s *MessageService) SendWelcomeMessage(ctx context.Context, phoneNumber, name string) (*SendMessageResult, error) {
	if name == "" { name = "Valued Customer" }
	template := sentdm.MessageSendParamsTemplate{ID: sentdm.String("welcome-template-id"), Name: sentdm.String("welcome"), Parameters: map[string]string{"name": name}}
	result, err := s.sentClient.SendMessage(ctx, []string{phoneNumber}, template, []string{"whatsapp"})
	if err != nil { return nil, err }
	s.logger.Info("welcome message sent", zap.String("phoneNumber", phoneNumber), zap.String("messageId", result.MessageID))
	return &SendMessageResult{MessageID: result.MessageID, Status: result.Status}, nil
}

// internal/service/webhook.go
package service

import (
	"context"
	"fmt"
	"github.com/sentdm/sent-dm-go"
	"github.com/sentdm/sent-dm-go/sent-echo-app/pkg/sentclient"
	"go.uber.org/zap"
)

type WebhookService struct {
	sentClient *sentclient.Client
	logger     *zap.Logger
	secret     string
}

func NewWebhookService(sentClient *sentclient.Client, logger *zap.Logger, secret string) *WebhookService {
	return &WebhookService{sentClient: sentClient, logger: logger.Named("webhook-service"), secret: secret}
}

func (s *WebhookService) VerifySignature(body, signature string) bool {
	if signature == "" { s.logger.Warn("missing webhook signature"); return false }
	return s.sentClient.VerifySignature(body, signature, s.secret)
}

func (s *WebhookService) ProcessEvent(ctx context.Context, body string) error {
	event, err := s.sentClient.ParseEvent(body)
	if err != nil { return fmt.Errorf("failed to parse event: %w", err) }
	s.logger.Info("processing webhook event", zap.String("type", event.Type), zap.String("eventId", event.ID))

	switch event.Type {
	case "message.delivered":
		s.logger.Info("message delivered", zap.String("messageId", event.Data.MessageID))
	case "message.failed":
		s.logger.Error("message failed", zap.String("messageId", event.Data.MessageID), zap.String("error", event.Data.Error.Message))
	case "message.status.updated":
		s.logger.Info("message status updated", zap.String("messageId", event.Data.MessageID), zap.String("status", event.Data.Status))
	default:
		s.logger.Warn("unhandled event type", zap.String("type", event.Type))
	}
	return nil
}

Handlers

// internal/handler/health.go
package handler

import (
	"net/http"
	"time"
	"github.com/labstack/echo/v4"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
)

type HealthHandler struct{ startTime time.Time }

func NewHealthHandler() *HealthHandler { return &HealthHandler{startTime: time.Now()} }
func (h *HealthHandler) Register(e *echo.Echo) {
	e.GET("/health", func(c echo.Context) error {
		return c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]interface{}{
			"status": "healthy", "uptime": time.Since(h.startTime).String(), "timestamp": time.Now().Unix(),
		}))
	})
	e.GET("/ready", func(c echo.Context) error {
		return c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]interface{}{"status": "ready"}))
	})
}

// internal/handler/message.go
package handler

import (
	"net/http"
	"github.com/go-playground/validator/v10"
	"github.com/labstack/echo/v4"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/service"
	"go.uber.org/zap"
)

type MessageHandler struct {
	messageService *service.MessageService
	logger         *zap.Logger
}

func NewMessageHandler(messageService *service.MessageService, logger *zap.Logger) *MessageHandler {
	return &MessageHandler{messageService: messageService, logger: logger.Named("message-handler")}
}

func (h *MessageHandler) Register(e *echo.Echo) {
	g := e.Group("/api/v1/messages")
	g.POST("/send", h.SendMessage)
	g.POST("/welcome", h.SendWelcome)
}

func (h *MessageHandler) SendMessage(c echo.Context) error {
	var req model.SendMessageRequest
	if err := c.Bind(&req); err != nil { return model.BindValidationError(err) }
	if err := c.Validate(&req); err != nil { return model.BindValidationError(err) }

	result, err := h.messageService.SendMessage(c.Request().Context(), &req)
	if err != nil {
		h.logger.Error("failed to send message", zap.Error(err))
		return err
	}
	return c.JSON(http.StatusOK, model.NewSuccessResponse(result))
}

func (h *MessageHandler) SendWelcome(c echo.Context) error {
	var req model.WelcomeMessageRequest
	if err := c.Bind(&req); err != nil { return model.BindValidationError(err) }
	if err := c.Validate(&req); err != nil { return model.BindValidationError(err) }

	result, err := h.messageService.SendWelcomeMessage(c.Request().Context(), req.PhoneNumber, req.Name)
	if err != nil {
		h.logger.Error("failed to send welcome message", zap.Error(err))
		return err
	}
	return c.JSON(http.StatusOK, model.NewSuccessResponse(result))
}

// internal/handler/webhook.go
package handler

import (
	"io"
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/service"
	"go.uber.org/zap"
)

type WebhookHandler struct {
	webhookService *service.WebhookService
	logger         *zap.Logger
}

func NewWebhookHandler(webhookService *service.WebhookService, logger *zap.Logger) *WebhookHandler {
	return &WebhookHandler{webhookService: webhookService, logger: logger.Named("webhook-handler")}
}

func (h *WebhookHandler) Register(e *echo.Echo) {
	e.POST("/webhooks/sent", h.HandleWebhook)
}

func (h *WebhookHandler) HandleWebhook(c echo.Context) error {
	body, err := io.ReadAll(c.Request().Body)
	if err != nil { return model.BindValidationError(err) }

	signature := c.Request().Header.Get("X-Webhook-Signature")
	if !h.webhookService.VerifySignature(string(body), signature) {
		h.logger.Warn("invalid webhook signature", zap.String("ip", c.RealIP()))
		return model.ErrUnauthorized
	}

	if err := h.webhookService.ProcessEvent(c.Request().Context(), string(body)); err != nil {
		h.logger.Error("failed to process webhook", zap.Error(err))
		return &model.HTTPError{Code: http.StatusBadRequest, ErrorCode: "PROCESSING_ERROR", Message: "Failed to process webhook", Details: err.Error()}
	}
	return c.JSON(http.StatusOK, model.NewSuccessResponse(map[string]bool{"received": true}))
}

Main Application

// cmd/api/main.go
package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/config"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/handler"
	appmiddleware "github.com/sentdm/sent-dm-go/sent-echo-app/internal/middleware"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/service"
	appvalidator "github.com/sentdm/sent-dm-go/sent-echo-app/internal/validator"
	"github.com/sentdm/sent-dm-go/sent-echo-app/pkg/sentclient"
	"go.uber.org/zap"
)

func main() {
	cfg, err := config.Load()
	if err != nil { panic(err) }

	logger, err := config.NewLogger(cfg.Log)
	if err != nil { panic(err) }
	defer logger.Sync()

	sentClient := sentclient.New(cfg.Sent.APIKey, logger)
	messageService := service.NewMessageService(sentClient, logger)
	webhookService := service.NewWebhookService(sentClient, logger, cfg.Sent.WebhookSecret)

	e := echo.New()
	e.HideBanner = true
	e.Validator = appvalidator.New()
	e.HTTPErrorHandler = appmiddleware.ErrorHandler(logger)

	e.Use(middleware.RequestID())
	e.Use(appmiddleware.Logger(logger))
	e.Use(middleware.Recover())
	e.Use(middleware.CORS())
	e.Use(middleware.Secure())
	e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(cfg.RateLimit.RequestsPerSecond)))

	handler.NewHealthHandler().Register(e)
	handler.NewMessageHandler(messageService, logger).Register(e)
	handler.NewWebhookHandler(webhookService, logger).Register(e)

	srv := &http.Server{
		Addr: ":" + cfg.Server.Port, Handler: e,
		ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout, IdleTimeout: cfg.Server.IdleTimeout,
	}

	go func() {
		logger.Info("starting server", zap.String("port", cfg.Server.Port))
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.Fatal("server failed", zap.Error(err))
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	logger.Info("shutting down...")
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		logger.Fatal("forced shutdown", zap.Error(err))
	}
	logger.Info("server exited")
}

Testing

// internal/handler/message_test.go
package handler

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"github.com/labstack/echo/v4"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/model"
	"github.com/sentdm/sent-dm-go/sent-echo-app/internal/service"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go.uber.org/zap"
)

type MockMessageService struct{ mock.Mock }
func (m *MockMessageService) SendMessage(ctx context.Context, req *model.SendMessageRequest) (*service.SendMessageResult, error) {
	args := m.Called(ctx, req)
	if args.Get(0) == nil { return nil, args.Error(1) }
	return args.Get(0).(*service.SendMessageResult), args.Error(1)
}
func (m *MockMessageService) SendWelcomeMessage(ctx context.Context, phoneNumber, name string) (*service.SendMessageResult, error) {
	args := m.Called(ctx, phoneNumber, name)
	if args.Get(0) == nil { return nil, args.Error(1) }
	return args.Get(0).(*service.SendMessageResult), args.Error(1)
}

func TestMessageHandler_SendMessage(t *testing.T) {
	tests := []struct {
		name       string
		body       interface{}
		mockSetup  func(*MockMessageService)
		wantStatus int
		wantSuccess bool
	}{
		{
			name: "success",
			body: model.SendMessageRequest{To: []string{"+1234567890"}, Template: model.TemplateRequest{ID: "tpl-123", Name: "welcome"}},
			mockSetup: func(m *MockMessageService) {
				m.On("SendMessage", mock.Anything, mock.Anything).Return(&service.SendMessageResult{MessageID: "msg_123", Status: "pending"}, nil)
			},
			wantStatus: 200, wantSuccess: true,
		},
		{
			name:       "invalid body",
			body:       "invalid",
			mockSetup:  func(m *MockMessageService) {},
			wantStatus: 400, wantSuccess: false,
		},
		{
			name: "service error",
			body: model.SendMessageRequest{To: []string{"+1234567890"}, Template: model.TemplateRequest{ID: "tpl-123"}},
			mockSetup: func(m *MockMessageService) {
				m.On("SendMessage", mock.Anything, mock.Anything).Return(nil, errors.New("failed"))
			},
			wantStatus: 500, wantSuccess: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			e := echo.New()
			mockSvc := new(MockMessageService)
			tt.mockSetup(mockSvc)

			h := NewMessageHandler(mockSvc, zap.NewNop())
			body, _ := json.Marshal(tt.body)
			req := httptest.NewRequest(http.MethodPost, "/api/v1/messages/send", bytes.NewReader(body))
			req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			err := h.SendMessage(c)
			if err != nil { e.HTTPErrorHandler(err, c) }

			assert.Equal(t, tt.wantStatus, rec.Code)
			var resp map[string]interface{}
			json.Unmarshal(rec.Body.Bytes(), &resp)
			assert.Equal(t, tt.wantSuccess, resp["success"])
			mockSvc.AssertExpectations(t)
		})
	}
}

Environment Variables

VariableDescriptionRequiredDefault
SENT_DM_API_KEYSent DM API keyYes-
SENT_DM_WEBHOOK_SECRETWebhook signature secretYes-
PORTServer portNo8080
SERVER_READ_TIMEOUTRequest read timeoutNo5s
SERVER_WRITE_TIMEOUTResponse write timeoutNo10s
ENVIRONMENTEnvironment nameNodevelopment
LOG_LEVELLog levelNoinfo
LOG_FORMATLog format (json/console)Nojson
RATE_LIMIT_RPSRate limit per secondNo10

Example .env

PORT=8080
SERVER_READ_TIMEOUT=5s
SERVER_WRITE_TIMEOUT=10s
ENVIRONMENT=development
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secret_here
LOG_LEVEL=info
LOG_FORMAT=json
RATE_LIMIT_RPS=10

Docker

# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o main ./cmd/api

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - ENVIRONMENT=production
      - SENT_DM_API_KEY=${SENT_DM_API_KEY}
      - SENT_DM_WEBHOOK_SECRET=${SENT_DM_WEBHOOK_SECRET}
      - LOG_LEVEL=info
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

go.mod

module github.com/sentdm/sent-dm-go/sent-echo-app

go 1.22

require (
	github.com/go-playground/validator/v10 v10.18.0
	github.com/kelseyhightower/envconfig v1.4.0
	github.com/labstack/echo/v4 v4.11.4
	github.com/sentdm/sent-dm-go v0.7.0
	github.com/stretchr/testify v1.9.0
	go.uber.org/zap v1.27.0
)

Next Steps

On this page