Core Concepts ​
This guide covers the fundamental concepts and patterns used in the goserve PostgreSQL example.
Table of Contents ​
- Request
- Payload
- Response
- Middleware
- Providers
- DTOs (Data Transfer Objects)
- Models
- Controllers
- Services
- Config
- Module
- Router
- Startup
- Database Operations
- Caching
- Docker
Request ​
Requests are handled using goserve's network utilities for extracting parameters, query strings, and request bodies.
// Example: Extracting path parameter "id"
uuidParam, err := network.ReqParams[coredto.UUID](ctx)
// Example: Extracting JSON request body
body, err := network.ReqBody[dto.CreateBlog](ctx)
pagination, err := network.ReqQuery[coredto.Pagination](ctx)The framework provides a few built-in validation for request DTOs using coredto package.
// blog/id/:id
type UUID struct {
Id string `uri:"id" binding:"required" validate:"required,uuid"`
ID uuid.UUID `uri:"-" validate:"-"`
}
func (d *UUID) GetValue() *UUID {
id, err := uuid.Parse(d.Id)
if err == nil {
d.ID = id
}
return d
}
// blogs/latest?page=1&limit=10
type Pagination struct {
Page int64 `form:"page" binding:"required" validate:"required,min=1,max=1000"`
Limit int64 `form:"limit" binding:"required" validate:"required,min=1,max=1000"`
}
// blog/slug/:slug
type Slug struct {
Slug string `uri:"slug" validate:"required,min=3,max=200"`
}Payload ​
The payload is a context wrapper that helps store and retrieve request-scoped data such as the authenticated user and API key.
// common/payload.go
const (
payloadApiKey string = "apikey"
payloadUser string = "user"
payloadKeystore string = "keystore"
)
type ContextPayload interface {
SetApiKey(ctx *gin.Context, value *authModel.ApiKey)
MustGetApiKey(ctx *gin.Context) *authModel.ApiKey
SetUser(ctx *gin.Context, value *userModel.User)
MustGetUser(ctx *gin.Context) *userModel.User
SetKeystore(ctx *gin.Context, value *authModel.Keystore)
MustGetKeystore(ctx *gin.Context) *authModel.Keystore
}
type payload struct{}
func NewContextPayload() ContextPayload {
return &payload{}
}
func (u *payload) SetApiKey(ctx *gin.Context, value *authModel.ApiKey) {
ctx.Set(payloadApiKey, value)
}
func (u *payload) MustGetApiKey(ctx *gin.Context) *authModel.ApiKey {
value, ok := ctx.MustGet(payloadApiKey).(*authModel.ApiKey)
if !ok {
panic(errors.New(payloadApiKey + " missing in context"))
}
return value
}
func (u *payload) SetUser(ctx *gin.Context, value *userModel.User) {
ctx.Set(payloadUser, value)
}
func (u *payload) MustGetUser(ctx *gin.Context) *userModel.User {
value, ok := ctx.MustGet(payloadUser).(*userModel.User)
if !ok {
panic(errors.New(payloadUser + " missing for context"))
}
return value
}
func (u *payload) SetKeystore(ctx *gin.Context, value *authModel.Keystore) {
ctx.Set(payloadKeystore, value)
}
func (u *payload) MustGetKeystore(ctx *gin.Context) *authModel.Keystore {
value, ok := ctx.MustGet(payloadKeystore).(*authModel.Keystore)
if !ok {
panic(errors.New(payloadKeystore + " missing for context"))
}
return value
}These are used in middlewares and controllers to set and get the API key, user, and keystore associated with the request.
Example
func (c *controller) postBlogHandler(ctx *gin.Context) {
body, err := network.ReqBody[dto.CreateBlog](ctx)
if err != nil {
network.SendBadRequestError(ctx, err.Error(), err)
return
}
// Get the authenticated user from the context set by the authentication middleware
user := c.MustGetUser(ctx)
b, err := c.service.CreateBlog(body, user)
if err != nil {
network.SendMixedError(ctx, err)
return
}
network.SendSuccessDataResponse(ctx, "blog created successfully", b)
}Response ​
Responses are sent using goserve's network package for consistent formatting.
Response Structure
type ResCode string
const (
success_code ResCode = "10000"
failue_code ResCode = "10001"
)
type response[T any] struct {
ResCode ResCode `json:"code" binding:"required"`
Status int `json:"status" binding:"required"`
Message string `json:"message" binding:"required"`
Data *T `json:"data,omitempty" binding:"required,omitempty"`
}Helper functions to send responses are defined in the network package:
// Send success response with data
func SendSuccessDataResponse[T any](ctx *gin.Context, message string, data T)
// Send bad request error response
func SendBadRequestError(ctx *gin.Context, message string, err error)
// Send not found error response
func SendNotFoundError(ctx *gin.Context, message string, err error)
// Send internal server error response
func SendInternalServerError(ctx *gin.Context, message string, err error)
// Send unauthorized error response
func SendUnauthorizedError(ctx *gin.Context, message string, err error)
// Send forbidden error response
func SendForbiddenError(ctx *gin.Context, message string, err error)
// Send mixed error response based on error type
func SendMixedError(ctx *gin.Context, err error)Middleware ​
Middleware functions are used for authentication, authorization, logging, etc. They can be defined and applied at the controller or route level. There are predefined interfaces for middleware in goserve:
type RootMiddleware interface {
Attach(engine *gin.Engine)
Handler(ctx *gin.Context)
}
type Param0MiddlewareProvider interface {
Middleware() gin.HandlerFunc
}
type Param1MiddlewareProvider[T any] interface {
Middleware(param1 T) gin.HandlerFunc
}
type Param2MiddlewareProvider[T any, V any] interface {
Middleware(param1 T, param2 V) gin.HandlerFunc
}
type Param3MiddlewareProvider[T any, V any, W any] interface {
Middleware(param1 T, param2 V, param3 W) gin.HandlerFunc
}
type ParamNMiddlewareProvider[T any] interface {
Middleware(params ...T) gin.HandlerFunc
}Root Middlewares ​
They are applied globally for all routes.
keyProtection ​
This middleware checks for the presence and validity of the x-api-key header in incoming requests.
// api/auth/middleware/keyprotection.go
type keyProtection struct {
common.ContextPayload
authService auth.Service
}
func NewKeyProtection(authService auth.Service) network.RootMiddleware {
return &keyProtection{
ContextPayload: common.NewContextPayload(),
authService: authService,
}
}
func (m *keyProtection) Attach(engine *gin.Engine) {
engine.Use(m.Handler)
}
func (m *keyProtection) Handler(ctx *gin.Context) {
key := ctx.GetHeader(network.ApiKeyHeader)
if len(key) == 0 {
network.SendUnauthorizedError(ctx, "permission denied: missing x-api-key header", nil)
return
}
apikey, err := m.authService.FindApiKey(key)
if err != nil {
network.SendForbiddenError(ctx, "permission denied: invalid x-api-key", err)
return
}
m.SetApiKey(ctx, apikey)
ctx.Next()
}Error Handling Middleware ​
The framework includes a global error catcher middleware to handle panics and send structured error responses.
type errorCatcher struct {
}
func NewErrorCatcher() network.RootMiddleware {
return &errorCatcher{}
}
func (m *errorCatcher) Attach(engine *gin.Engine) {
engine.Use(m.Handler)
}
func (m *errorCatcher) Handler(ctx *gin.Context) {
defer func() {
if r := recover(); r != nil {
if err, ok := r.(error); ok {
network.SendInternalServerError(ctx, err.Error(), err)
} else {
network.SendInternalServerError(ctx, "something went wrong", nil)
}
ctx.Abort()
}
}()
ctx.Next()
}Notfound Middleware ​
This middleware handles 404 Not Found errors for unmatched routes.
type notFound struct {
}
func NewNotFound() network.RootMiddleware {
return ¬Found{}
}
func (m *notFound) Attach(engine *gin.Engine) {
engine.NoRoute(m.Handler)
}
func (m *notFound) Handler(ctx *gin.Context) {
network.SendNotFoundError(ctx, "url not found", nil)
}Providers ​
Providers are specialized middleware for providing authentication and authorization handlers.
type AuthenticationProvider Param0MiddlewareProvider
type AuthorizationProvider ParamNMiddlewareProvider[string]Authentication Provider ​
This middleware validates JWT tokens in the Authorization header.
type authenticationProvider struct {
common.ContextPayload
authService auth.Service
userService user.Service
}
func NewAuthenticationProvider(authService auth.Service, userService user.Service) network.AuthenticationProvider {
return &authenticationProvider{
ContextPayload: common.NewContextPayload(),
authService: authService,
userService: userService,
}
}
func (m *authenticationProvider) Middleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
authHeader := ctx.GetHeader(network.AuthorizationHeader)
if len(authHeader) == 0 {
network.SendUnauthorizedError(ctx, "permission denied: missing Authorization", nil)
return
}
token := utils.ExtractBearerToken(authHeader)
if token == "" {
network.SendUnauthorizedError(ctx, "permission denied: invalid Authorization", nil)
return
}
claims, err := m.authService.VerifyToken(token)
if err != nil {
network.SendUnauthorizedError(ctx, err.Error(), err)
return
}
valid := m.authService.ValidateClaims(claims)
if !valid {
network.SendUnauthorizedError(ctx, "permission denied: invalid claims", nil)
return
}
userId, err := postgres.NewObjectID(claims.Subject)
if err != nil {
network.SendUnauthorizedError(ctx, "permission denied: invalid claims subject", nil)
return
}
user, err := m.userService.FindUserById(userId)
if err != nil {
network.SendUnauthorizedError(ctx, "permission denied: claims subject does not exists", err)
return
}
keystore, err := m.authService.FindKeystore(user, claims.ID)
if err != nil || keystore == nil {
network.SendUnauthorizedError(ctx, "permission denied: invalid access token", err)
return
}
m.SetUser(ctx, user)
m.SetKeystore(ctx, keystore)
ctx.Next()
}
}Authorization Provider ​
This middleware checks if the authenticated user has the required role to access the endpoint.
type authorizationProvider struct {
common.ContextPayload
}
func NewAuthorizationProvider() network.AuthorizationProvider {
return &authorizationProvider{
ContextPayload: common.NewContextPayload(),
}
}
func (m *authorizationProvider) Middleware(roleNames ...string) gin.HandlerFunc {
return func(ctx *gin.Context) {
if len(roleNames) == 0 {
network.SendForbiddenError(ctx, "permission denied: role missing", nil)
return
}
user := m.MustGetUser(ctx)
hasRole := false
for _, code := range roleNames {
for _, role := range user.RoleDocs {
if role.Code == model.RoleCode(code) {
hasRole = true
break
}
}
if hasRole {
break
}
}
if !hasRole {
network.SendForbiddenError(ctx, "permission denied: does not have suffient role", nil)
return
}
ctx.Next()
}
}DTOs (Data Transfer Objects) ​
DTOs define the structure of data transferred between layers. DTO can be created by implementing the Dto[T] or DtoV[T] interfaces or plain structs.
type Dto[T any] interface {
GetValue() *T
}
type DtoV[T any] interface {
Dto[T]
ValidateErrors(errs validator.ValidationErrors) ([]string, error)
}Request/Response DTOs ​
Most often, plain structs are sufficient, since the validations are handled by the architecture out of the box. But for more complex cases, you can implement your own interfaces to ValidateErrors.
Example:
type InfoSample struct {
ID uuid.UUID `json:"_id" binding:"required"`
Field string `json:"field" binding:"required"`
CreatedAt time.Time `json:"createdAt" binding:"required"`
}Note: The response DTOs are also validated using the binding tags.
Models ​
Models represent PostgreSQL records.
PostgreSQL Record Model ​
const SampleTableName = "samples"
type Sample struct {
ID uuid.UUID // id
Field string // field
Status bool // status
CreatedAt time.Time // created_at
UpdatedAt time.Time // updated_at
}Controllers ​
Controllers are responsible for handling HTTP requests and responses. They define API endpoints and delegate business logic to services.
Controller Interface ​
type Controller interface {
Path() string
Authentication() gin.HandlerFunc
Authorization(role string) gin.HandlerFunc
MountRoutes(group *gin.RouterGroup)
}Basic Controller ​
type controller struct {
network.Controller
common.ContextPayload
service Service
}
func NewController(
authMFunc network.AuthenticationProvider,
authorizeMFunc network.AuthorizationProvider,
service Service,
) network.Controller {
return &controller{
Controller: network.NewController("/sample", authMFunc, authorizeMFunc),
ContextPayload: common.NewContextPayload(),
service: service,
}
}
func (c *controller) MountRoutes(group *gin.RouterGroup) {
group.GET("/id/:id", c.getSampleHandler)
}
func (c *controller) getSampleHandler(ctx *gin.Context) {
uuidParam, err := network.ReqParams[coredto.UUID](ctx)
if err != nil {
network.SendBadRequestError(ctx, err.Error(), err)
return
}
sample, err := c.service.FindSample(uuidParam.ID)
if err != nil {
network.SendNotFoundError(ctx, "sample not found", err)
return
}
data, err := utility.MapTo[dto.InfoSample](sample)
if err != nil {
network.SendInternalServerError(ctx, "something went wrong", err)
return
}
network.SendSuccessDataResponse(ctx, "success", data)
}Services ​
Services contain business logic and coordinate between controllers and the database.
Service Pattern ​
type Service interface {
FindSample(id uuid.UUID) (*model.Sample, error)
}
type service struct {
db postgres.Database
infoSampleCache redis.Cache[dto.InfoSample]
}
func NewService(db postgres.Database, store redis.Store) Service {
return &service{
db: db,
infoSampleCache: redis.NewCache[dto.InfoSample](store),
}
}
func (s *service) FindSample(id uuid.UUID) (*model.Sample, error) {
ctx := context.Background()
query := `
SELECT
id,
field,
status,
created_at,
updated_at
FROM samples
WHERE id = $1
`
var m model.Sample
err := s.db.Pool().QueryRow(ctx, query, id).
Scan(
&m.ID,
&m.Field,
&m.Status,
&m.CreatedAt,
&m.UpdatedAt,
)
if err != nil {
return nil, err
}
return &m, nil
}Config ​
Configuration is managed using environment variables loaded into a struct.
// config/env.go
type Env struct {
// server
GoMode string `mapstructure:"GO_MODE"`
ServerHost string `mapstructure:"SERVER_HOST"`
ServerPort uint16 `mapstructure:"SERVER_PORT"`
// database
DBHost string `mapstructure:"DB_HOST"`
DBName string `mapstructure:"DB_NAME"`
DBPort uint16 `mapstructure:"DB_PORT"`
DBUser string `mapstructure:"DB_USER"`
DBUserPwd string `mapstructure:"DB_USER_PWD"`
DBMinPoolSize uint16 `mapstructure:"DB_MIN_POOL_SIZE"`
DBMaxPoolSize uint16 `mapstructure:"DB_MAX_POOL_SIZE"`
DBQueryTimeout uint16 `mapstructure:"DB_QUERY_TIMEOUT_SEC"`
// redis
RedisHost string `mapstructure:"REDIS_HOST"`
RedisPort uint16 `mapstructure:"REDIS_PORT"`
RedisPwd string `mapstructure:"REDIS_PASSWORD"`
RedisDB int `mapstructure:"REDIS_DB"`
// keys
RSAPrivateKeyPath string `mapstructure:"RSA_PRIVATE_KEY_PATH"`
RSAPublicKeyPath string `mapstructure:"RSA_PUBLIC_KEY_PATH"`
// Token
AccessTokenValiditySec uint64 `mapstructure:"ACCESS_TOKEN_VALIDITY_SEC"`
RefreshTokenValiditySec uint64 `mapstructure:"REFRESH_TOKEN_VALIDITY_SEC"`
TokenIssuer string `mapstructure:"TOKEN_ISSUER"`
TokenAudience string `mapstructure:"TOKEN_AUDIENCE"`
}
func NewEnv(filename string, override bool) *Env {
env := Env{}
viper.SetConfigFile(filename)
if override {
viper.AutomaticEnv()
}
err := viper.ReadInConfig()
if err != nil {
log.Fatal("Error reading environment file", err)
}
err = viper.Unmarshal(&env)
if err != nil {
log.Fatal("Error loading environment file", err)
}
return &env
}Module ​
Modules act as a dependency injection container, initializing and providing services, controllers, and middlewares. It implements the network.Module interface:
type BaseModule[T any] interface {
GetInstance() *T
RootMiddlewares() []RootMiddleware
AuthenticationProvider() AuthenticationProvider
AuthorizationProvider() AuthorizationProvider
}
type Module[T any] interface {
BaseModule[T]
Controllers() []Controller
}Project Module ​
// startup/module.go
type Module network.Module[module]
type module struct {
Context context.Context
Env *config.Env
DB postgres.Database
Store redis.Store
UserService user.Service
AuthService auth.Service
BlogService blog.Service
HealthService health.Service
}
func (m *module) GetInstance() *module {
return m
}
// OpenControllers are controllers that do not require api key authentication
func (m *module) OpenControllers() []network.Controller {
return []network.Controller{health.NewController(m.HealthService)}
}
func (m *module) Controllers() []network.Controller {
return []network.Controller{
auth.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.AuthService),
user.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.UserService),
blog.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), m.BlogService),
author.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), author.NewService(m.DB, m.BlogService)),
editor.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), editor.NewService(m.DB, m.UserService)),
blogs.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), blogs.NewService(m.DB, m.Store)),
contact.NewController(m.AuthenticationProvider(), m.AuthorizationProvider(), contact.NewService(m.DB)),
}
}
func (m *module) RootMiddlewares() []network.RootMiddleware {
return []network.RootMiddleware{
coreMW.NewErrorCatcher(), // NOTE: this should be the first handler to be mounted
authMW.NewKeyProtection(m.AuthService),
coreMW.NewNotFound(),
}
}
func (m *module) AuthenticationProvider() network.AuthenticationProvider {
return authMW.NewAuthenticationProvider(m.AuthService, m.UserService)
}
func (m *module) AuthorizationProvider() network.AuthorizationProvider {
return authMW.NewAuthorizationProvider()
}
func NewModule(context context.Context, env *config.Env, db postgres.Database, store redis.Store) Module {
userService := user.NewService(db)
authService := auth.NewService(db, env, userService)
blogService := blog.NewService(db, store, userService)
healthService := health.NewService()
return &module{
Context: context,
Env: env,
DB: db,
Store: store,
UserService: userService,
AuthService: authService,
BlogService: blogService,
HealthService: healthService,
}
}Router ​
The router sets up the Gin engine, applies middlewares, and mounts controllers.
// startup/router.go create function
module := NewModule(context, env, db, store)
router := network.NewRouter(env.GoMode)
router.RegisterValidationParsers(network.CustomTagNameFunc())
router.LoadControllers(module.GetInstance().OpenControllers())
router.LoadRootMiddlewares(module.RootMiddlewares())
router.LoadControllers(module.Controllers())Startup ​
The startup package initializes the server, database connections, and other components.
type Shutdown = func()
func Server() {
env := config.NewEnv(".env", true)
router, _, shutdown := create(env)
defer shutdown()
router.Start(env.ServerHost, env.ServerPort)
}
func create(env *config.Env) (network.Router, Module, Shutdown) {
context := context.Background()
dbConfig := postgres.DbConfig{
User: env.DBUser,
Pwd: env.DBUserPwd,
Host: env.DBHost,
Port: env.DBPort,
Name: env.DBName,
MinPoolSize: env.DBMinPoolSize,
MaxPoolSize: env.DBMaxPoolSize,
Timeout: time.Duration(env.DBQueryTimeout) * time.Second,
}
db := postgres.NewDatabase(context, dbConfig)
db.Connect()
redisConfig := redis.Config{
Host: env.RedisHost,
Port: env.RedisPort,
Pwd: env.RedisPwd,
DB: env.RedisDB,
}
store := redis.NewStore(context, &redisConfig)
store.Connect()
module := NewModule(context, env, db, store)
router := network.NewRouter(env.GoMode)
router.RegisterValidationParsers(network.CustomTagNameFunc())
router.LoadControllers(module.GetInstance().OpenControllers())
router.LoadRootMiddlewares(module.RootMiddlewares())
router.LoadControllers(module.Controllers())
shutdown := func() {
db.Disconnect()
store.Disconnect()
}
return router, module, shutdown
}Database Operations ​
MongoDB Query Patterns ​
goserve used pgx library for PostgreSQL operations. You can find the basic query patterns from the library documentation: github.com/jackc/pgx
// Single document queries
ctx := context.Background()
query := `
SELECT
id,
field,
status,
created_at,
updated_at
FROM samples
WHERE id = $1
`
var m model.Sample
err := s.db.Pool().QueryRow(ctx, query, id).
Scan(
&m.ID,
&m.Field,
&m.Status,
&m.CreatedAt,
&m.UpdatedAt,
)Database Migrations ​
Database migrations are managed using the golang-migrate/migrate tool. Migration files are located in the migrations/ directory. To run migrations, use the following command:
Connection Management ​
// startup/server.go create function
context := context.Background()
dbConfig := postgres.DbConfig{
User: env.DBUser,
Pwd: env.DBUserPwd,
Host: env.DBHost,
Port: env.DBPort,
Name: env.DBName,
MinPoolSize: env.DBMinPoolSize,
MaxPoolSize: env.DBMaxPoolSize,
Timeout: time.Duration(env.DBQueryTimeout) * time.Second,
}
db := postgres.NewDatabase(context, dbConfig)
db.Connect()
shutdown := func() {
db.Disconnect()
//...
}Caching ​
Redis Integration ​
// Cache configuration
type service struct {
publicBlogCache redis.Cache[dto.PublicBlog]
//...
}
func NewService(db postgres.Database, store redis.Store, userService user.Service) Service {
return &service{
publicBlogCache: redis.NewCache[dto.PublicBlog](store),
// ...
}
}
func (s *service) SetBlogDtoCacheById(blog *dto.PublicBlog) error {
key := "blog_" + blog.ID.Hex()
return s.publicBlogCache.SetJSON(key, blog, time.Duration(10*time.Minute))
}
func (s *service) GetBlogDtoCacheById(id primitive.ObjectID) (*dto.PublicBlog, error) {
key := "blog_" + id.Hex()
return s.publicBlogCache.GetJSON(key)
}Docker ​
The project includes Docker configurations for containerized deployment.
Dockerfile ​
# Use Go v1.25.6 as the base image
FROM golang:1.25.6-alpine
RUN apk add --no-cache curl
# Create a new user in the docker image
RUN adduser --disabled-password --gecos '' gouser
# Create a new directory for goserve files and set the path in the container
RUN mkdir -p /home/gouser/goserve
# Set the working directory in the container
WORKDIR /home/gouser/goserve
# Copy the project files into the container
COPY . .
# Set the ownership of the goserve directory to gouser
RUN chown -R gouser:gouser /home/gouser/goserve
# Switch to the gouser user
USER gouser
# Download dependencies and build the project
RUN go mod tidy
RUN go build -o build/server cmd/main.go
# Expose the server port (replace 8080 with your actual port)
EXPOSE 8080
# Command to run the server
CMD ["./build/server"]Docker-Compose ​
services:
goserver:
build:
context: .
dockerfile: Dockerfile
container_name: goserver-postgres
restart: unless-stopped
env_file: .env
ports:
- '${SERVER_PORT}:${SERVER_PORT}'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
networks:
- goserve-postgres-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
postgres:
image: postgres:18.1
restart: unless-stopped
env_file: .env
ports:
- '${DB_PORT}:5432'
volumes:
- dbdata:/data/db
# optional pg seed scripts
- ./.extra/setup/init-test-db.sql:/docker-entrypoint-initdb.d/init-test-db.sql:ro
- ./.extra/setup/pgseed.sql:/docker-entrypoint-initdb.d/pgseed.sql:ro
networks:
- goserve-postgres-network
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -h localhost -p 5432 -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""
]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
redis:
image: redis:8.4.0
restart: unless-stopped
env_file: .env
ports:
- '${REDIS_PORT}:6379'
command: redis-server --bind 0.0.0.0 --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD}
volumes:
- cache:/data/cache
networks:
- goserve-postgres-network
healthcheck:
test:
[
"CMD",
"redis-cli",
"-a", "${REDIS_PASSWORD}",
"ping"
]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
migrate:
image: migrate/migrate
env_file: .test.env
volumes:
- ./migrations:/migrations
depends_on:
postgres:
condition: service_healthy
networks:
- goserve-postgres-network
entrypoint: ["/bin/sh", "-c"]
command:
- |
migrate -path /migrations -database "postgres://$${DB_USER}:$${DB_USER_PWD}@postgres:5432/$${DB_NAME}?sslmode=disable" up
networks:
goserve-postgres-network:
driver: bridge
volumes:
dbdata:
cache:
driver: localBest Practices ​
PostgreSQL Best Practices ​
- Use UUID - use UUID for document identification
- Migrations - manage schema changes using migration tools
- Connection Pooling - configure min/max pool sizes for optimal performance
- Handle Errors - Proper error handling for PostgreSQL operations
Service Layer Best Practices ​
- Business Logic Only - Keep services focused on business rules
- Error Handling - Use structured errors with proper HTTP status codes
- Caching Strategy - Implement cache-aside pattern appropriately
API Design Best Practices ​
- RESTful Endpoints - Use proper HTTP methods and resource naming
- Consistent Responses - Use goserve's response helpers
- Input Validation - Validate requests at controller level
- Authentication - Protect sensitive endpoints with JWT
