N-API Template - Code Generation Reference
This document serves as a comprehensive reference for generating API code following the N-API Template standards. Use this guide to ensure consistency and adherence to established patterns.
Table of Contents
- Project Structure
- Main Application Entry Point
- Bootstrap Configuration
- Go Module Setup
- Configuration Files
- Port Layer (Request/Response Interfaces)
- Domain Model Pattern
- Repository Pattern
- Handler Pattern
- Request DTO Pattern
- Response DTO Pattern
- Routing Pattern
- Validation Pattern
- Database Schema
- Naming Conventions
- Error Handling
- Complete Example Workflow
- Development Workflow
Project Structure
When creating a new resource, follow this structure:
n-api-template/
├── main.go # Application entry point
├── go.mod # Go module dependencies
├── go.sum # Dependency checksums
├── configs/ # Configuration files
│ ├── config.yaml # Base configuration
│ ├── config.dev.yaml # Development environment
│ ├── config.sit.yaml # System Integration Test
│ ├── config.staging.yaml # Staging environment
│ ├── config.training.yaml # Training environment
│ ├── config.test.yaml # Test environment
│ └── config.prod.yaml # Production environment
├── bootstrap/
│ └── bootstrapper.go # Dependency injection modules
├── core/
│ ├── domain/
│ │ └── {resource}.go # Domain model
│ └── port/
│ ├── request.go # Common request structures
│ └── response.go # Common response structures
├── handler/
│ ├── {resource}.go # Handler with routes
│ ├── request.go # Request DTOs (add new structs here)
│ ├── request_*_validator.go # Auto-generated validators
│ └── response/
│ └── {resource}.go # Response DTOs
├── repo/
│ └── postgres/
│ └── {resource}.go # Repository/data access
├── db/
│ └── {resource}.sql # Database schema
└── docs/ # Swagger documentation (auto-generated)
Main Application Entry Point
Location: main.go
Purpose: Application entry point that initializes and starts the server with all dependencies.
Pattern:
package main
import (
"context"
"{project}/bootstrap"
bootstrapper "gitlab.cept.gov.in/it-2.0-common/n-api-bootstrapper"
)
func main() {
app := bootstrapper.New().Options(
// Add your FX modules here
bootstrap.FxHandler, // Register all handlers
bootstrap.FxRepo, // Register all repositories
// bootstrap.Fxvalidator, // Optional: custom validators
)
app.WithContext(context.Background()).Run()
}
Rules:
- Import your project's bootstrap package
- Import n-api-bootstrapper for application initialization
- Register all FX modules in
.Options()call - Pass
context.Background()toWithContext() - Call
.Run()to start the application - The bootstrapper automatically handles:
- Configuration loading
- Database connection
- Server initialization
- Graceful shutdown
- Signal handling
- Dependency injection
Example:
package main
import (
"context"
"pisapi/bootstrap"
bootstrapper "gitlab.cept.gov.in/it-2.0-common/n-api-bootstrapper"
)
func main() {
app := bootstrapper.New().Options(
bootstrap.FxHandler,
bootstrap.FxRepo,
)
app.WithContext(context.Background()).Run()
}
Bootstrap Configuration
Location: bootstrap/bootstrapper.go
Purpose: Defines Uber FX dependency injection modules for automatic wiring of dependencies.
Complete Pattern:
package bootstrap
import (
"go.uber.org/fx"
serverHandler "gitlab.cept.gov.in/it-2.0-common/n-api-server/handler"
handler "{project}/handler"
repo "{project}/repo/postgres"
)
// FxRepo module provides all repository implementations
var FxRepo = fx.Module(
"Repomodule",
fx.Provide(
repo.New{Resource1}Repository,
repo.New{Resource2}Repository,
// Add more repository constructors here
// repo.New{Resource3}Repository,
),
)
// FxHandler module provides all HTTP handlers
var FxHandler = fx.Module(
"Handlermodule",
fx.Provide(
// Each handler must be annotated to implement serverHandler.Handler interface
fx.Annotate(
handler.New{Resource1}Handler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
fx.Annotate(
handler.New{Resource2}Handler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
// Add more handler constructors here
// fx.Annotate(
// handler.New{Resource3}Handler,
// fx.As(new(serverHandler.Handler)),
// fx.ResultTags(serverHandler.ServerControllersGroupTag),
// ),
),
)
// Optional: Custom validator module (if using custom validators)
// var Fxvalidator = fx.Module(
// "Validatormodule",
// fx.Provide(
// // Add custom validator providers here
// ),
// )
Rules:
- Create separate FX modules for different concerns (Repo, Handler, Validator, etc.)
- Module naming convention:
Fx{ModuleName}(e.g., FxRepo, FxHandler) - Module string name:
"{ModuleName}module"(e.g., "Repomodule", "Handlermodule") - Use
fx.Provide()to register constructors - Handlers MUST be wrapped with
fx.Annotate()with:fx.As(new(serverHandler.Handler))- Converts to Handler interfacefx.ResultTags(serverHandler.ServerControllersGroupTag)- Groups handlers
- Repositories are provided directly without annotation
- Add comments to indicate where new resources should be added
- Dependencies are automatically injected based on constructor parameters
- Order of registration doesn't matter (FX resolves dependency graph)
Dependency Injection Flow:
- Bootstrapper creates database connection (*dblib.DB)
- Bootstrapper loads configuration (*config.Config)
- FxRepo provides repositories (injecting db and config)
- FxHandler provides handlers (injecting repositories)
- Server automatically discovers and registers all handlers
Example with Multiple Resources:
package bootstrap
import (
handler "pisapi/handler"
repo "pisapi/repo/postgres"
serverHandler "gitlab.cept.gov.in/it-2.0-common/n-api-server/handler"
"go.uber.org/fx"
)
var FxRepo = fx.Module(
"Repomodule",
fx.Provide(
repo.NewUserRepository,
repo.NewProductRepository,
repo.NewOrderRepository,
),
)
var FxHandler = fx.Module(
"Handlermodule",
fx.Provide(
fx.Annotate(
handler.NewUserHandler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
fx.Annotate(
handler.NewProductHandler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
fx.Annotate(
handler.NewOrderHandler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
),
)
Go Module Setup
Location: go.mod
Purpose: Defines Go module and manages dependencies.
Pattern:
module {project}
go 1.25.0
require (
github.com/Masterminds/squirrel v1.5.4
github.com/jackc/pgx/v5 v5.7.6
gitlab.cept.gov.in/it-2.0-common/api-config v0.0.17
gitlab.cept.gov.in/it-2.0-common/api-db v1.0.32
gitlab.cept.gov.in/it-2.0-common/n-api-bootstrapper v0.0.14
gitlab.cept.gov.in/it-2.0-common/n-api-log v0.0.1
gitlab.cept.gov.in/it-2.0-common/n-api-server v0.0.17
gitlab.cept.gov.in/it-2.0-common/n-api-validation v0.0.3
go.uber.org/fx v1.24.0
)
Core Dependencies:
github.com/Masterminds/squirrel- SQL query buildergithub.com/jackc/pgx/v5- PostgreSQL drivergitlab.cept.gov.in/it-2.0-common/api-config- Configuration managementgitlab.cept.gov.in/it-2.0-common/api-db- Database utilitiesgitlab.cept.gov.in/it-2.0-common/n-api-bootstrapper- Application bootstrappergitlab.cept.gov.in/it-2.0-common/n-api-log- Logging utilitiesgitlab.cept.gov.in/it-2.0-common/n-api-server- Server frameworkgitlab.cept.gov.in/it-2.0-common/n-api-validation- Validation frameworkgo.uber.org/fx- Dependency injection framework
Commands:
# Initialize new module
go mod init {project}
# Add dependency
go get package@version
# Update all dependencies
go get -u ./...
# Tidy up (remove unused, add missing)
go mod tidy
# Download dependencies
go mod download
# Verify checksums
go mod verify
Rules:
- Use semantic versioning for your module
- Pin exact versions in production
- Run
go mod tidyafter adding/removing imports - Commit both go.mod and go.sum
- Use private GitLab registry for internal packages
Configuration Files
Location: configs/config.yaml (and environment-specific variants)
Purpose: Application configuration for different environments.
Base Configuration Pattern (configs/config.yaml):
# Application name
appname: "{project-name}"
# Tracing configuration (OpenTelemetry)
trace:
enabled: false # Enable/disable distributed tracing
processor:
type: "otlp-grpc" # Export format: otlp-grpc or otlp-http
options:
host: "localhost:4317" # OpenTelemetry collector endpoint
sampler:
type: always-on # Sampling strategy: always-on, always-off, parent-based-trace-id-ratio
options:
ratio: 0.1 # Sample 10% of traces (if using ratio sampler)
# Cache configuration (Redis + Local)
cache:
# Redis settings
redisserver: "10.20.30.33:6379"
redispassword: ""
redisdbindex: 1
redisexpirationtime: 20m
# Local cache settings
lccapacity: 10000 # Maximum number of entries
lcnumshards: 20 # Number of shards for concurrent access
lcttl: 2m # Time to live for cache entries
lcevictionpercentage: 10 # Percentage to evict when full
lcminrefreshdelay: 15m # Minimum delay before refresh
lcmaxrefreshdelay: 30m # Maximum delay before refresh
lcretrybasedelay: 1s # Base delay for retries
lcbatchsize: 10 # Batch size for operations
lcbatchbuffertimeout: 30s # Batch buffer timeout
# Enable/disable cache layers
isredisenabled: true
islocalcacheenabled: false
# Database configuration (PostgreSQL)
db:
username: "postgres"
password: "your-password"
host: "localhost"
port: "5432"
database: "your-database"
schema: "public"
# Connection pool settings
maxconns: 10 # Maximum connections
minconns: 1 # Minimum connections
maxconnlifetime: 30 # Max connection lifetime (minutes)
maxconnidletime: 10 # Max idle time (minutes)
healthcheckperiod: 5 # Health check interval (minutes)
# Query timeouts
QueryTimeoutLow: 2s # Simple queries
QueryTimeoutMed: 5s # Complex queries/aggregations
# Application info (for Swagger)
info:
name: "{project-name}"
version: "1.0.0"
Environment-Specific Files:
config.dev.yaml- Development environmentconfig.test.yaml- Test environmentconfig.sit.yaml- System Integration Testconfig.training.yaml- Training environmentconfig.staging.yaml- Staging environmentconfig.prod.yaml- Production environment
Environment Override Example (config.prod.yaml):
# Production overrides (only specify what changes)
db:
host: "prod-db-server.example.com"
password: "${DB_PASSWORD}" # Use environment variable
maxconns: 50 # Higher for production
minconns: 10
trace:
enabled: true # Enable tracing in production
sampler:
type: parent-based-trace-id-ratio
options:
ratio: 0.1 # Sample 10% in production
cache:
redisserver: "prod-redis.example.com:6379"
redispassword: "${REDIS_PASSWORD}"
isredisenabled: true
islocalcacheenabled: true # Enable both layers in production
Configuration Access in Code:
// In repository or service
timeout := r.cfg.GetDuration("db.QueryTimeoutLow")
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Get string value
appName := cfg.GetString("appname")
// Get int value
maxConns := cfg.GetInt("db.maxconns")
// Get bool value
traceEnabled := cfg.GetBool("trace.enabled")
Environment Selection:
# Set via environment variable
export ENV=prod
go run main.go
# Or via command line flag
go run main.go -env=prod
Rules:
- Base config (config.yaml) contains all keys with defaults
- Environment configs only override specific values
- Use environment variables for secrets (${VAR_NAME})
- Never commit passwords/secrets to git
- Use duration format: 2s, 5m, 1h, etc.
- Database password should use environment variable in production
- Always have separate configs for each environment
- Cache and tracing can be disabled per environment
Port Layer (Request/Response Interfaces)
Location: core/port/request.go and core/port/response.go
Purpose: Defines common request/response structures and interfaces used across handlers.
Request Structures (core/port/request.go)
package port
// MetadataRequest provides common pagination and sorting parameters
// Embed this in list/search request structs
type MetadataRequest struct {
Skip uint64 `form:"skip,default=0" validate:"omitempty"`
Limit uint64 `form:"limit,default=10" validate:"omitempty"`
OrderBy string `form:"orderBy" validate:"omitempty"`
SortType string `form:"sortType" validate:"omitempty"`
}
Usage in Handlers:
// In handler/request.go
type ListUsersParams struct {
port.MetadataRequest
// Add additional filters here if needed
Status string `form:"status" validate:"omitempty"`
}
Response Structures (core/port/response.go)
package port
import "io"
// Standard status messages for all operations
var (
ListSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "list retrieved successfully", Success: true}
FetchSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "data retrieved successfully", Success: true}
CreateSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 201, Message: "resource created successfully", Success: true}
UpdateSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "resource updated successfully", Success: true}
DeleteSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "resource deleted successfully", Success: true}
CustomEnv StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "This is environment specific", Success: true}
)
// OTP-related status constants
var (
OTPSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "OTP generated successfully", Success: true}
OTPAuthSuccess StatusCodeAndMessage = StatusCodeAndMessage{StatusCode: 200, Message: "OTP authenticated successfully", Success: true}
)
// StatusCodeAndMessage is embedded in all response structs
// Provides consistent status code, success flag, and message
type StatusCodeAndMessage struct {
StatusCode int `json:"status_code"`
Success bool `json:"success"`
Message string `json:"message"`
}
// Status returns HTTP status code (interface compliance)
func (s StatusCodeAndMessage) Status() int {
return s.StatusCode
}
func (s StatusCodeAndMessage) ResponseType() string {
return "standard"
}
func (s StatusCodeAndMessage) GetContentType() string {
return "application/json"
}
func (s StatusCodeAndMessage) GetContentDisposition() string {
return ""
}
func (s StatusCodeAndMessage) Object() []byte {
return nil
}
// FileResponse for file downloads/uploads
type FileResponse struct {
ContentDisposition string
ContentType string
Data []byte // Memory-based payload
Reader io.ReadCloser // Optional streaming source
}
func (s FileResponse) GetContentType() string {
return s.ContentType
}
func (s FileResponse) GetContentDisposition() string {
return s.ContentDisposition
}
func (s FileResponse) ResponseType() string {
return "file"
}
func (s FileResponse) Status() int {
return 200
}
func (s FileResponse) Object() []byte {
return s.Data
}
// Stream copies Reader to w if available; else writes Data
func (s FileResponse) Stream(w io.Writer) error {
if s.Reader == nil {
if len(s.Data) > 0 {
_, err := w.Write(s.Data)
return err
}
return nil
}
defer s.Reader.Close()
_, err := io.Copy(w, s.Reader)
return err
}
// MetaDataResponse provides pagination metadata
// Embed this in list response structs
type MetaDataResponse struct {
Skip uint64 `json:"skip,default=0"`
Limit uint64 `json:"limit,default=10"`
OrderBy string `json:"order_by,omitempty"`
SortType string `json:"sort_type,omitempty"`
TotalRecordsCount int `json:"total_records_count,omitempty"`
ReturnedRecordsCount uint64 `json:"returned_records_count"`
}
// Helper function to create metadata response
func NewMetaDataResponse(skip, limit, total uint64) MetaDataResponse {
return MetaDataResponse{
Skip: skip,
Limit: limit,
TotalRecordsCount: int(total),
ReturnedRecordsCount: limit,
}
}
Usage in Response DTOs:
// In handler/response/user.go
type UsersListResponse struct {
port.StatusCodeAndMessage `json:",inline"` // Adds status_code, success, message
port.MetaDataResponse `json:",inline"` // Adds pagination metadata
Data []UserResponse `json:"data"`
}
type UserCreateResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data UserResponse `json:"data"`
}
Rules:
- Use predefined
StatusCodeAndMessageconstants (ListSuccess, CreateSuccess, etc.) - Embed
port.MetadataRequestfor list endpoints (provides pagination) - Embed
port.StatusCodeAndMessagein all response structs - Embed
port.MetaDataResponsefor list responses - Use
json:",inline"to flatten embedded structs - FileResponse for serving files/downloads
- Never modify port layer structs directly (they're shared)
Domain Model Pattern
Location: core/domain/{resource}.go
Purpose: Represents the business entity with database mapping.
Pattern:
package domain
import "time"
type {Resource} struct {
ID int64 `json:"id" db:"id"`
Field1 string `json:"field1" db:"field1"`
Field2 string `json:"field2" db:"field2"`
Field3 int `json:"field3" db:"field3"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
Rules:
- Use
snake_casefor JSON and DB tags - Always include
ID,CreatedAt,UpdatedAt - Match DB column names exactly in
db:tags - Export all fields (capitalize first letter)
- Use appropriate Go types (int64 for IDs, time.Time for timestamps)
Example:
package domain
import "time"
type Product struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Price float64 `json:"price" db:"price"`
Stock int `json:"stock" db:"stock"`
CategoryID int64 `json:"category_id" db:"category_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
Repository Pattern
Location: repo/postgres/{resource}.go
Purpose: Handles all database operations for the resource.
Pattern:
package repo
import (
"context"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
"gitlab.cept.gov.in/it-2.0-common/api-config/config"
"gitlab.cept.gov.in/it-2.0-common/api-db/dblib"
"{project}/core/domain"
)
type {Resource}Repository struct {
db *dblib.DB
cfg *config.Config
}
func New{Resource}Repository(db *dblib.DB, cfg *config.Config) *{Resource}Repository {
return &{Resource}Repository{
db: db,
cfg: cfg,
}
}
const {resource}Table = "{resources}"
// Create inserts a new {resource}
func (r *{Resource}Repository) Create(ctx context.Context, data domain.{Resource}) (domain.{Resource}, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Insert({resource}Table).
Columns("field1", "field2", "field3").
Values(data.Field1, data.Field2, data.Field3).
Suffix("RETURNING id, field1, field2, field3, created_at, updated_at").
PlaceholderFormat(sq.Dollar)
var result domain.{Resource}
err := dblib.Insert(ctx, r.db, query, &result)
return result, err
}
// FindByID retrieves a {resource} by ID
func (r *{Resource}Repository) FindByID(ctx context.Context, id int64) (domain.{Resource}, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Select("id", "field1", "field2", "field3", "created_at", "updated_at").
From({resource}Table).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
var result domain.{Resource}
err := dblib.SelectOne(ctx, r.db, query, &result)
if err != nil {
if err == pgx.ErrNoRows {
return result, err
}
return result, err
}
return result, nil
}
// List retrieves all {resources} with pagination
func (r *{Resource}Repository) List(ctx context.Context, skip, limit int64, orderBy, sortType string) ([]domain.{Resource}, int64, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutMed"))
defer cancel()
// Count query
countQuery := sq.Select("COUNT(*)").
From({resource}Table).
PlaceholderFormat(sq.Dollar)
var totalCount int64
err := dblib.SelectOne(ctx, r.db, countQuery, &totalCount)
if err != nil {
return nil, 0, err
}
// Data query
query := sq.Select("id", "field1", "field2", "field3", "created_at", "updated_at").
From({resource}Table).
OrderBy(orderBy + " " + sortType).
Limit(uint64(limit)).
Offset(uint64(skip)).
PlaceholderFormat(sq.Dollar)
var results []domain.{Resource}
err = dblib.SelectRows(ctx, r.db, query, &results)
if err != nil {
return nil, 0, err
}
return results, totalCount, nil
}
// Update updates a {resource} by ID
func (r *{Resource}Repository) Update(ctx context.Context, id int64, field1, field2 *string, field3 *int) (domain.{Resource}, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Update({resource}Table).
Set("updated_at", time.Now()).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
// Only update non-nil fields
if field1 != nil {
query = query.Set("field1", *field1)
}
if field2 != nil {
query = query.Set("field2", *field2)
}
if field3 != nil {
query = query.Set("field3", *field3)
}
query = query.Suffix("RETURNING id, field1, field2, field3, created_at, updated_at")
var result domain.{Resource}
err := dblib.Update(ctx, r.db, query, &result)
return result, err
}
// Delete deletes a {resource} by ID
func (r *{Resource}Repository) Delete(ctx context.Context, id int64) error {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Delete({resource}Table).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
return dblib.Delete(ctx, r.db, query)
}
Rules:
- Always inject
*dblib.DBand*config.Config - Use context with timeout for all queries
- Use Squirrel query builder (alias
sq) - Always use
.PlaceholderFormat(sq.Dollar)for PostgreSQL - Use
dblib.Insert(),dblib.SelectOne(),dblib.SelectRows(),dblib.Update(),dblib.Delete() - Handle
pgx.ErrNoRowsfor not found errors - For updates: use pointers for optional fields, only update non-nil fields
- Always set
updated_atin update queries - Return domain models, not DTOs
Handler Pattern
Location: handler/{resource}.go
Purpose: Defines HTTP routes and handles HTTP requests.
Pattern:
package handler
import (
"github.com/jackc/pgx/v5"
serverHandler "gitlab.cept.gov.in/it-2.0-common/n-api-server/handler"
serverRoute "gitlab.cept.gov.in/it-2.0-common/n-api-server/route"
"{project}/core/port"
resp "{project}/handler/response"
"{project}/repo"
)
type {Resource}Handler struct {
*serverHandler.Base
svc *repo.{Resource}Repository
}
func New{Resource}Handler(svc *repo.{Resource}Repository) *{Resource}Handler {
base := serverHandler.New("{Resources}").
SetPrefix("/v1").
AddPrefix("")
return &{Resource}Handler{
Base: base,
svc: svc,
}
}
// Routes defines all routes for this handler
func (h *{Resource}Handler) Routes() []serverRoute.Route {
return []serverRoute.Route{
serverRoute.POST("/{resources}", h.Create{Resource}).Name("Create {Resource}"),
serverRoute.GET("/{resources}", h.List{Resources}).Name("List {Resources}"),
serverRoute.GET("/{resources}/:id", h.Get{Resource}ByID).Name("Get {Resource} By ID"),
serverRoute.PUT("/{resources}/:id", h.Update{Resource}ByID).Name("Update {Resource} By ID"),
serverRoute.DELETE("/{resources}/:id", h.Delete{Resource}ByID).Name("Delete {Resource} By ID"),
}
}
// Create{Resource} creates a new {resource}
func (h *{Resource}Handler) Create{Resource}(sctx *serverRoute.Context, req Create{Resource}Request) (*resp.{Resource}CreateResponse, error) {
// Convert request to domain model
data := req.ToDomain()
// Call repository
result, err := h.svc.Create(sctx.Ctx, data)
if err != nil {
sctx.Log.Error("failed to create {resource}", "error", err)
return nil, err
}
// Convert to response
return &resp.{Resource}CreateResponse{
StatusCodeAndMessage: port.CreateSuccess,
Data: resp.New{Resource}Response(result),
}, nil
}
// List{Resources} retrieves all {resources}
func (h *{Resource}Handler) List{Resources}(sctx *serverRoute.Context, req List{Resources}Params) (*resp.{Resources}ListResponse, error) {
// Call repository
results, totalCount, err := h.svc.List(sctx.Ctx, req.Skip, req.Limit, req.OrderBy, req.SortType)
if err != nil {
sctx.Log.Error("failed to list {resources}", "error", err)
return nil, err
}
// Convert to response
return &resp.{Resources}ListResponse{
StatusCodeAndMessage: port.ListSuccess,
MetaDataResponse: port.MetaDataResponse{
TotalCount: totalCount,
Count: int64(len(results)),
Skip: req.Skip,
Limit: req.Limit,
},
Data: resp.New{Resources}Response(results),
}, nil
}
// Get{Resource}ByID retrieves a {resource} by ID
func (h *{Resource}Handler) Get{Resource}ByID(sctx *serverRoute.Context, req {Resource}IDUri) (*resp.{Resource}FetchResponse, error) {
// Call repository
result, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("{resource} not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get {resource}", "error", err)
return nil, err
}
// Convert to response
return &resp.{Resource}FetchResponse{
StatusCodeAndMessage: port.FetchSuccess,
Data: resp.New{Resource}Response(result),
}, nil
}
// Update{Resource}ByID updates a {resource} by ID
func (h *{Resource}Handler) Update{Resource}ByID(sctx *serverRoute.Context, req Update{Resource}Request) (*resp.{Resource}UpdateResponse, error) {
// Check if {resource} exists
_, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("{resource} not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get {resource}", "error", err)
return nil, err
}
// Convert non-empty fields to pointers
var field1, field2 *string
var field3 *int
if req.Field1 != "" {
field1 = &req.Field1
}
if req.Field2 != "" {
field2 = &req.Field2
}
if req.Field3 != 0 {
field3 = &req.Field3
}
// Call repository
result, err := h.svc.Update(sctx.Ctx, req.ID, field1, field2, field3)
if err != nil {
sctx.Log.Error("failed to update {resource}", "error", err)
return nil, err
}
// Convert to response
return &resp.{Resource}UpdateResponse{
StatusCodeAndMessage: port.UpdateSuccess,
Data: resp.New{Resource}Response(result),
}, nil
}
// Delete{Resource}ByID deletes a {resource} by ID
func (h *{Resource}Handler) Delete{Resource}ByID(sctx *serverRoute.Context, req {Resource}IDUri) (*resp.{Resource}DeleteResponse, error) {
// Check if {resource} exists
_, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("{resource} not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get {resource}", "error", err)
return nil, err
}
// Call repository
err = h.svc.Delete(sctx.Ctx, req.ID)
if err != nil {
sctx.Log.Error("failed to delete {resource}", "error", err)
return nil, err
}
// Return success response
return &resp.{Resource}DeleteResponse{
StatusCodeAndMessage: port.DeleteSuccess,
}, nil
}
Rules:
- Embed
*serverHandler.Base - Inject repository as
svc - Use
serverHandler.New()with resource name (plural, capitalized) - Set prefix to
/v1for API versioning - Handler signature:
(sctx *serverRoute.Context, req RequestType) (*ResponseType, error) - Always log errors before returning
- Check for
pgx.ErrNoRowsfor 404 errors - For updates: check existence first, then update
- For deletes: check existence first, then delete
- Use
sctx.Ctxfor context,sctx.Logfor logging
Request DTO Pattern
Location: handler/request.go
Purpose: Defines request data transfer objects with validation.
Pattern:
package handler
import "{project}/core/domain"
// Create{Resource}Request represents the request body for creating a {resource}
type Create{Resource}Request struct {
Field1 string `json:"field1" validate:"required"`
Field2 string `json:"field2" validate:"required"`
Field3 int `json:"field3" validate:"required"`
}
func (r Create{Resource}Request) ToDomain() domain.{Resource} {
return domain.{Resource}{
Field1: r.Field1,
Field2: r.Field2,
Field3: r.Field3,
}
}
// Update{Resource}Request represents the request body for updating a {resource}
type Update{Resource}Request struct {
ID int64 `uri:"id" validate:"required"`
Field1 string `json:"field1" validate:"omitempty"`
Field2 string `json:"field2" validate:"omitempty"`
Field3 int `json:"field3" validate:"omitempty"`
}
// {Resource}IDUri represents the URI parameter for {resource} ID
type {Resource}IDUri struct {
ID int64 `uri:"id" validate:"required"`
}
// List{Resources}Params represents query parameters for listing {resources}
type List{Resources}Params struct {
port.MetadataRequest
}
Rules:
- Add all request structs to
handler/request.go - Use
validate:"required"for mandatory fields - Use
validate:"omitempty"for optional fields (updates) - Use
uri:tag for URL parameters - Use
json:tag for JSON body fields - Use
form:tag for form data - Embed
port.MetadataRequestfor list endpoints (provides Skip, Limit, OrderBy, SortType) - Include
ToDomain()method for create requests - Use
snake_casefor JSON field names
Validation Tags:
required- Field must not be emptyomitempty- Field is optionalemail- Must be valid email formatmin=N- Minimum value/lengthmax=N- Maximum value/lengthoneof=val1 val2- Must be one of specified values
Response DTO Pattern
Location: handler/response/{resource}.go
Purpose: Defines response data transfer objects.
Pattern:
package response
import (
"{project}/core/domain"
"{project}/core/port"
)
// {Resource}Response represents a {resource} in API responses
type {Resource}Response struct {
ID int64 `json:"id"`
Field1 string `json:"field1"`
Field2 string `json:"field2"`
Field3 int `json:"field3"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// New{Resource}Response converts domain model to response DTO
func New{Resource}Response(d domain.{Resource}) {Resource}Response {
return {Resource}Response{
ID: d.ID,
Field1: d.Field1,
Field2: d.Field2,
Field3: d.Field3,
CreatedAt: d.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: d.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}
// New{Resources}Response converts slice of domain models to response DTOs
func New{Resources}Response(data []domain.{Resource}) []{Resource}Response {
res := make([]{Resource}Response, 0, len(data))
for _, d := range data {
res = append(res, New{Resource}Response(d))
}
return res
}
// {Resource}CreateResponse represents the response for creating a {resource}
type {Resource}CreateResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data {Resource}Response `json:"data"`
}
// {Resource}FetchResponse represents the response for fetching a single {resource}
type {Resource}FetchResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data {Resource}Response `json:"data"`
}
// {Resources}ListResponse represents the response for listing {resources}
type {Resources}ListResponse struct {
port.StatusCodeAndMessage `json:",inline"`
port.MetaDataResponse `json:",inline"`
Data []{Resource}Response `json:"data"`
}
// {Resource}UpdateResponse represents the response for updating a {resource}
type {Resource}UpdateResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data {Resource}Response `json:"data"`
}
// {Resource}DeleteResponse represents the response for deleting a {resource}
type {Resource}DeleteResponse struct {
port.StatusCodeAndMessage `json:",inline"`
}
Rules:
- Create separate response structs for each operation (Create, Fetch, List, Update, Delete)
- Embed
port.StatusCodeAndMessagefor status info - Embed
port.MetaDataResponsefor list responses (pagination) - Use
json:",inline"for embedded structs - Provide conversion functions:
New{Resource}Response()andNew{Resources}Response() - Format timestamps as strings:
"2006-01-02 15:04:05" - Use
snake_casefor JSON field names
Standard Response Structures:
- Create:
{StatusCodeAndMessage, Data: {Resource}Response} - Fetch:
{StatusCodeAndMessage, Data: {Resource}Response} - List:
{StatusCodeAndMessage, MetaDataResponse, Data: []{Resource}Response} - Update:
{StatusCodeAndMessage, Data: {Resource}Response} - Delete:
{StatusCodeAndMessage}(no data)
Routing Pattern
Routes Definition:
func (h *{Resource}Handler) Routes() []serverRoute.Route {
return []serverRoute.Route{
serverRoute.POST("/{resources}", h.Create{Resource}).Name("Create {Resource}"),
serverRoute.GET("/{resources}", h.List{Resources}).Name("List {Resources}"),
serverRoute.GET("/{resources}/:id", h.Get{Resource}ByID).Name("Get {Resource} By ID"),
serverRoute.PUT("/{resources}/:id", h.Update{Resource}ByID).Name("Update {Resource} By ID"),
serverRoute.DELETE("/{resources}/:id", h.Delete{Resource}ByID).Name("Delete {Resource} By ID"),
}
}
RESTful Conventions:
| Method | Path | Handler | Purpose |
|---|---|---|---|
| POST | /{resources} | Create{Resource} | Create new resource |
| GET | /{resources} | List{Resources} | List all resources |
| GET | /{resources}/:id | Get{Resource}ByID | Get single resource |
| PUT | /{resources}/:id | Update{Resource}ByID | Update resource |
| DELETE | /{resources}/:id | Delete{Resource}ByID | Delete resource |
Rules:
- Use plural for collection endpoints (
/users) - Use
:idfor path parameters - Use
.Name()for Swagger documentation - Prefix is set in handler constructor (
/v1) - Final URL:
/v1/{resources}or/v1/{resources}/:id
Validation Pattern
Auto-generated Validators:
- Run
govalidtool to generate validators - Generated files:
handler/request_*_validator.go - Implements
Validatorinterface withValidate()method
Manual Generation:
# Navigate to handler directory
cd handler
# Run govalid
govalid
Generated Validator Example:
// Auto-generated by govalid
func (r Create{Resource}Request) Validate() error {
var validationErrors []ValidationError
if r.Field1 == "" {
validationErrors = append(validationErrors, ValidationError{
Reason: "Field1 is required",
Path: "field1",
Type: "required",
Value: r.Field1,
})
}
if len(validationErrors) > 0 {
return ValidationErrors(validationErrors)
}
return nil
}
Rules:
- Validators are auto-generated, do not modify
- Add validation tags to request structs
- Re-run
govalidafter modifying request structs - Framework automatically calls
Validate()before handler execution
Database Schema
Location: db/{resource}.sql
Pattern:
CREATE TABLE IF NOT EXISTS {resources} (
id SERIAL PRIMARY KEY,
field1 VARCHAR(255) NOT NULL,
field2 VARCHAR(255) NOT NULL,
field3 INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Add indexes for frequently queried fields
CREATE INDEX IF NOT EXISTS idx_{resources}_field1 ON {resources}(field1);
-- Add unique constraints if needed
ALTER TABLE {resources} ADD CONSTRAINT unique_{resources}_field1 UNIQUE (field1);
Rules:
- Use
SERIALfor auto-incrementing IDs - Always include
created_atandupdated_atwithDEFAULT NOW() - Use appropriate data types:
VARCHAR(N)for stringsINTEGERfor whole numbersDECIMAL(P,S)for money/decimalsTIMESTAMPfor dates/timesBOOLEANfor true/falseTEXTfor large text
- Add indexes for foreign keys and frequently queried fields
- Add unique constraints where applicable
- Use
IF NOT EXISTSto make migrations idempotent
Naming Conventions
Package Names
domain- Business entitieshandler- HTTP handlersresponse- Response DTOs (subpackage of handler)repo- Repository interfacespostgres- PostgreSQL implementations (subpackage of repo)
Type Names
- Domain:
{Resource}(e.g.,User,Product) - Repository:
{Resource}Repository(e.g.,UserRepository) - Handler:
{Resource}Handler(e.g.,UserHandler) - Request:
Create{Resource}Request,Update{Resource}Request,{Resource}IDUri,List{Resources}Params - Response:
{Resource}Response,{Resource}CreateResponse,{Resources}ListResponse
Function Names
- Constructor:
New{Resource}Repository,New{Resource}Handler - Handler methods:
Create{Resource},List{Resources},Get{Resource}ByID,Update{Resource}ByID,Delete{Resource}ByID - Repository methods:
Create,FindByID,List,Update,Delete - Response converter:
New{Resource}Response,New{Resources}Response
Field Names
- Go:
PascalCase(e.g.,FirstName) - JSON:
snake_case(e.g.,first_name) - Database:
snake_case(e.g.,first_name) - URL params:
snake_case(e.g.,:id,:user_id)
Route Names
- Paths:
/{resources}(plural, lowercase) - Route names:
"Create {Resource}","List {Resources}"(for Swagger)
Error Handling
In Handlers:
result, err := h.svc.SomeMethod(sctx.Ctx, params)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("resource not found", "id", id)
return nil, err
}
sctx.Log.Error("failed to perform operation", "error", err)
return nil, err
}
In Repositories:
err := dblib.SelectOne(ctx, r.db, query, &result)
if err != nil {
if err == pgx.ErrNoRows {
return result, err // Let handler decide how to handle
}
return result, err
}
return result, nil
Rules:
- Always log errors before returning
- Use descriptive log messages
- Include relevant context in logs (IDs, parameters)
- Return errors directly (framework handles HTTP status codes)
- Check for
pgx.ErrNoRowsfor 404 scenarios - Don't wrap errors unnecessarily
Complete Example Workflow
When creating a new resource called Product, follow these steps:
Step 1: Create Domain Model
File: core/domain/product.go
package domain
import "time"
type Product struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Price float64 `json:"price" db:"price"`
Stock int `json:"stock" db:"stock"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
Step 2: Create Database Schema
File: db/products.sql
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
Step 3: Create Repository
File: repo/postgres/product.go
package repo
import (
"context"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/jackc/pgx/v5"
"gitlab.cept.gov.in/it-2.0-common/api-config/config"
"gitlab.cept.gov.in/it-2.0-common/api-db/dblib"
"n-api-template/core/domain"
)
type ProductRepository struct {
db *dblib.DB
cfg *config.Config
}
func NewProductRepository(db *dblib.DB, cfg *config.Config) *ProductRepository {
return &ProductRepository{
db: db,
cfg: cfg,
}
}
const productTable = "products"
func (r *ProductRepository) Create(ctx context.Context, data domain.Product) (domain.Product, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Insert(productTable).
Columns("name", "description", "price", "stock").
Values(data.Name, data.Description, data.Price, data.Stock).
Suffix("RETURNING id, name, description, price, stock, created_at, updated_at").
PlaceholderFormat(sq.Dollar)
var result domain.Product
err := dblib.Insert(ctx, r.db, query, &result)
return result, err
}
func (r *ProductRepository) FindByID(ctx context.Context, id int64) (domain.Product, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Select("id", "name", "description", "price", "stock", "created_at", "updated_at").
From(productTable).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
var result domain.Product
err := dblib.SelectOne(ctx, r.db, query, &result)
if err != nil {
return result, err
}
return result, nil
}
func (r *ProductRepository) List(ctx context.Context, skip, limit int64, orderBy, sortType string) ([]domain.Product, int64, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutMed"))
defer cancel()
countQuery := sq.Select("COUNT(*)").
From(productTable).
PlaceholderFormat(sq.Dollar)
var totalCount int64
err := dblib.SelectOne(ctx, r.db, countQuery, &totalCount)
if err != nil {
return nil, 0, err
}
query := sq.Select("id", "name", "description", "price", "stock", "created_at", "updated_at").
From(productTable).
OrderBy(orderBy + " " + sortType).
Limit(uint64(limit)).
Offset(uint64(skip)).
PlaceholderFormat(sq.Dollar)
var results []domain.Product
err = dblib.SelectRows(ctx, r.db, query, &results)
if err != nil {
return nil, 0, err
}
return results, totalCount, nil
}
func (r *ProductRepository) Update(ctx context.Context, id int64, name, description *string, price *float64, stock *int) (domain.Product, error) {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Update(productTable).
Set("updated_at", time.Now()).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
if name != nil {
query = query.Set("name", *name)
}
if description != nil {
query = query.Set("description", *description)
}
if price != nil {
query = query.Set("price", *price)
}
if stock != nil {
query = query.Set("stock", *stock)
}
query = query.Suffix("RETURNING id, name, description, price, stock, created_at, updated_at")
var result domain.Product
err := dblib.Update(ctx, r.db, query, &result)
return result, err
}
func (r *ProductRepository) Delete(ctx context.Context, id int64) error {
ctx, cancel := context.WithTimeout(ctx, r.cfg.GetDuration("db.QueryTimeoutLow"))
defer cancel()
query := sq.Delete(productTable).
Where(sq.Eq{"id": id}).
PlaceholderFormat(sq.Dollar)
return dblib.Delete(ctx, r.db, query)
}
Step 4: Create Request DTOs
File: handler/request.go (add to existing file)
type CreateProductRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
Price float64 `json:"price" validate:"required"`
Stock int `json:"stock" validate:"required"`
}
func (r CreateProductRequest) ToDomain() domain.Product {
return domain.Product{
Name: r.Name,
Description: r.Description,
Price: r.Price,
Stock: r.Stock,
}
}
type UpdateProductRequest struct {
ID int64 `uri:"id" validate:"required"`
Name string `json:"name" validate:"omitempty"`
Description string `json:"description" validate:"omitempty"`
Price float64 `json:"price" validate:"omitempty"`
Stock int `json:"stock" validate:"omitempty"`
}
type ProductIDUri struct {
ID int64 `uri:"id" validate:"required"`
}
type ListProductsParams struct {
port.MetadataRequest
}
Step 5: Create Response DTOs
File: handler/response/product.go
package response
import (
"n-api-template/core/domain"
"n-api-template/core/port"
)
type ProductResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func NewProductResponse(d domain.Product) ProductResponse {
return ProductResponse{
ID: d.ID,
Name: d.Name,
Description: d.Description,
Price: d.Price,
Stock: d.Stock,
CreatedAt: d.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: d.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}
func NewProductsResponse(data []domain.Product) []ProductResponse {
res := make([]ProductResponse, 0, len(data))
for _, d := range data {
res = append(res, NewProductResponse(d))
}
return res
}
type ProductCreateResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data ProductResponse `json:"data"`
}
type ProductFetchResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data ProductResponse `json:"data"`
}
type ProductsListResponse struct {
port.StatusCodeAndMessage `json:",inline"`
port.MetaDataResponse `json:",inline"`
Data []ProductResponse `json:"data"`
}
type ProductUpdateResponse struct {
port.StatusCodeAndMessage `json:",inline"`
Data ProductResponse `json:"data"`
}
type ProductDeleteResponse struct {
port.StatusCodeAndMessage `json:",inline"`
}
Step 6: Create Handler
File: handler/product.go
package handler
import (
"github.com/jackc/pgx/v5"
serverHandler "gitlab.cept.gov.in/it-2.0-common/n-api-server/handler"
serverRoute "gitlab.cept.gov.in/it-2.0-common/n-api-server/route"
"n-api-template/core/port"
resp "n-api-template/handler/response"
"n-api-template/repo"
)
type ProductHandler struct {
*serverHandler.Base
svc *repo.ProductRepository
}
func NewProductHandler(svc *repo.ProductRepository) *ProductHandler {
base := serverHandler.New("Products").
SetPrefix("/v1").
AddPrefix("")
return &ProductHandler{
Base: base,
svc: svc,
}
}
func (h *ProductHandler) Routes() []serverRoute.Route {
return []serverRoute.Route{
serverRoute.POST("/products", h.CreateProduct).Name("Create Product"),
serverRoute.GET("/products", h.ListProducts).Name("List Products"),
serverRoute.GET("/products/:id", h.GetProductByID).Name("Get Product By ID"),
serverRoute.PUT("/products/:id", h.UpdateProductByID).Name("Update Product By ID"),
serverRoute.DELETE("/products/:id", h.DeleteProductByID).Name("Delete Product By ID"),
}
}
func (h *ProductHandler) CreateProduct(sctx *serverRoute.Context, req CreateProductRequest) (*resp.ProductCreateResponse, error) {
data := req.ToDomain()
result, err := h.svc.Create(sctx.Ctx, data)
if err != nil {
sctx.Log.Error("failed to create product", "error", err)
return nil, err
}
return &resp.ProductCreateResponse{
StatusCodeAndMessage: port.CreateSuccess,
Data: resp.NewProductResponse(result),
}, nil
}
func (h *ProductHandler) ListProducts(sctx *serverRoute.Context, req ListProductsParams) (*resp.ProductsListResponse, error) {
results, totalCount, err := h.svc.List(sctx.Ctx, req.Skip, req.Limit, req.OrderBy, req.SortType)
if err != nil {
sctx.Log.Error("failed to list products", "error", err)
return nil, err
}
return &resp.ProductsListResponse{
StatusCodeAndMessage: port.ListSuccess,
MetaDataResponse: port.MetaDataResponse{
TotalCount: totalCount,
Count: int64(len(results)),
Skip: req.Skip,
Limit: req.Limit,
},
Data: resp.NewProductsResponse(results),
}, nil
}
func (h *ProductHandler) GetProductByID(sctx *serverRoute.Context, req ProductIDUri) (*resp.ProductFetchResponse, error) {
result, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("product not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get product", "error", err)
return nil, err
}
return &resp.ProductFetchResponse{
StatusCodeAndMessage: port.FetchSuccess,
Data: resp.NewProductResponse(result),
}, nil
}
func (h *ProductHandler) UpdateProductByID(sctx *serverRoute.Context, req UpdateProductRequest) (*resp.ProductUpdateResponse, error) {
_, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("product not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get product", "error", err)
return nil, err
}
var name, description *string
var price *float64
var stock *int
if req.Name != "" {
name = &req.Name
}
if req.Description != "" {
description = &req.Description
}
if req.Price != 0 {
price = &req.Price
}
if req.Stock != 0 {
stock = &req.Stock
}
result, err := h.svc.Update(sctx.Ctx, req.ID, name, description, price, stock)
if err != nil {
sctx.Log.Error("failed to update product", "error", err)
return nil, err
}
return &resp.ProductUpdateResponse{
StatusCodeAndMessage: port.UpdateSuccess,
Data: resp.NewProductResponse(result),
}, nil
}
func (h *ProductHandler) DeleteProductByID(sctx *serverRoute.Context, req ProductIDUri) (*resp.ProductDeleteResponse, error) {
_, err := h.svc.FindByID(sctx.Ctx, req.ID)
if err != nil {
if err == pgx.ErrNoRows {
sctx.Log.Error("product not found", "id", req.ID)
return nil, err
}
sctx.Log.Error("failed to get product", "error", err)
return nil, err
}
err = h.svc.Delete(sctx.Ctx, req.ID)
if err != nil {
sctx.Log.Error("failed to delete product", "error", err)
return nil, err
}
return &resp.ProductDeleteResponse{
StatusCodeAndMessage: port.DeleteSuccess,
}, nil
}
Step 7: Register Dependencies
File: bootstrap/bootstrapper.go
var FxRepo = fx.Module(
"Repomodule",
fx.Provide(
repo.NewUserRepository,
repo.NewProductRepository, // Add this line
),
)
var FxHandler = fx.Module(
"Handlermodule",
fx.Provide(
fx.Annotate(
handler.NewUserHandler,
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
fx.Annotate(
handler.NewProductHandler, // Add this block
fx.As(new(serverHandler.Handler)),
fx.ResultTags(serverHandler.ServerControllersGroupTag),
),
),
)
Step 8: Generate Validators
cd handler
govalid
Step 9: Run Migrations
# Apply database schema
psql -U username -d database -f db/products.sql
Step 10: Test Endpoints
# Start the server
go run main.go
# Test endpoints
# Create
curl -X POST http://localhost:8080/v1/products \
-H "Content-Type: application/json" \
-d '{"name":"Product 1","description":"Description","price":99.99,"stock":100}'
# List
curl http://localhost:8080/v1/products
# Get by ID
curl http://localhost:8080/v1/products/1
# Update
curl -X PUT http://localhost:8080/v1/products/1 \
-H "Content-Type: application/json" \
-d '{"name":"Updated Product"}'
# Delete
curl -X DELETE http://localhost:8080/v1/products/1
Quick Reference Checklist
When creating a new API resource, ensure you:
- Create domain model in
core/domain/{resource}.go - Create database schema in
db/{resources}.sql - Create repository in
repo/postgres/{resource}.gowith:-
Createmethod -
FindByIDmethod -
Listmethod -
Updatemethod -
Deletemethod
-
- Add request DTOs to
handler/request.go:-
Create{Resource}Request -
Update{Resource}Request -
{Resource}IDUri -
List{Resources}Params
-
- Create response DTOs in
handler/response/{resource}.go:-
{Resource}Response - Conversion functions
- Operation-specific responses (Create, Fetch, List, Update, Delete)
-
- Create handler in
handler/{resource}.gowith:- Constructor
-
Routes()method - CRUD handler methods
- Register in
bootstrap/bootstrapper.go:- Add repository to
FxRepo - Add handler to
FxHandler
- Add repository to
- Generate validators with
govalid - Run database migrations
- Test all endpoints
Common Patterns Summary
REST Operations:
- POST /{resources} - Create
- GET /{resources} - List (with pagination)
- GET /{resources}/:id - Get single
- PUT /{resources}/:id - Update
- DELETE /{resources}/:id - Delete
Response Codes:
- 200 OK - Successful GET, PUT, DELETE
- 201 Created - Successful POST
- 400 Bad Request - Validation errors
- 404 Not Found - Resource not found
- 500 Internal Server Error - Server errors
Standard Response Format:
{
"status_code": 200,
"success": true,
"message": "operation successful",
"data": {...}
}
List Response Format:
{
"status_code": 200,
"success": true,
"message": "list retrieved successfully",
"total_count": 100,
"count": 10,
"skip": 0,
"limit": 10,
"data": [...]
}
Notes
- Replace
{Resource}with your actual resource name (e.g.,Product,Order) - Replace
{resources}with plural lowercase (e.g.,products,orders) - Replace
{project}with your actual project module name - All timestamps are stored in UTC
- Pagination defaults: Skip=0, Limit=10, OrderBy="id", SortType="asc"
- Query timeouts prevent long-running queries from blocking
- Use Squirrel for all SQL query building (type-safe, composable)
- All database operations must use context with timeout
- Framework handles request binding, validation, and response serialization automatically
Development Workflow
Initial Project Setup
# 1. Create project directory
mkdir {project-name}
cd {project-name}
# 2. Initialize Go module
go mod init {project}
# 3. Create directory structure
mkdir -p bootstrap configs core/domain core/port handler/response repo/postgres db docs
# 4. Install core dependencies
go get gitlab.cept.gov.in/it-2.0-common/n-api-bootstrapper@latest
go get gitlab.cept.gov.in/it-2.0-common/n-api-server@latest
go get gitlab.cept.gov.in/it-2.0-common/api-db@latest
go get gitlab.cept.gov.in/it-2.0-common/api-config@latest
go get github.com/Masterminds/squirrel@latest
go get github.com/jackc/pgx/v5@latest
go get go.uber.org/fx@latest
# 5. Tidy up dependencies
go mod tidy
# 6. Create config files (copy from template)
cp path/to/template/configs/* configs/
# 7. Create main.go and bootstrap files
# (Follow patterns in this document)
Adding a New Resource
Step-by-Step Checklist:
-
Create Domain Model (
core/domain/{resource}.go)# Create the file and add domain struct with db tags -
Create Database Schema (
db/{resources}.sql)# Write CREATE TABLE statement with indexes -
Apply Database Migration
psql -U username -d database -f db/{resources}.sql -
Create Repository (
repo/postgres/{resource}.go)# Implement Create, FindByID, List, Update, Delete methods -
Add Request DTOs (
handler/request.go)# Add Create, Update, ID, and List request structs -
Create Response DTOs (
handler/response/{resource}.go)# Add response structs and conversion functions -
Create Handler (
handler/{resource}.go)# Implement handler with all CRUD methods -
Register Dependencies (
bootstrap/bootstrapper.go)# Add repository to FxRepo # Add handler to FxHandler -
Generate Validators
cd handler govalid cd .. -
Test Endpoints
# Start server and test with curl or Postman
Running the Application
# Development (uses config.yaml or config.dev.yaml)
go run main.go
# Specify environment
ENV=dev go run main.go # Development
ENV=test go run main.go # Test
ENV=sit go run main.go # System Integration Test
ENV=staging go run main.go # Staging
ENV=prod go run main.go # Production
# Build binary
go build -o bin/app main.go
# Run binary
./bin/app
# Build with version info
go build -ldflags "-X main.Version=1.0.0" -o bin/app main.go
Database Operations
# Connect to database
psql -U username -d database
# Run migration
psql -U username -d database -f db/{resource}.sql
# Check tables
\dt
# Describe table
\d {resources}
# Query data
SELECT * FROM {resources};
# Drop table (careful!)
DROP TABLE IF EXISTS {resources};
Testing Endpoints
# Create
curl -X POST http://localhost:8080/v1/{resources} \
-H "Content-Type: application/json" \
-d '{
"field1": "value1",
"field2": "value2",
"field3": 123
}'
# List with pagination
curl "http://localhost:8080/v1/{resources}?skip=0&limit=10&orderBy=id&sortType=asc"
# Get by ID
curl http://localhost:8080/v1/{resources}/1
# Update
curl -X PUT http://localhost:8080/v1/{resources}/1 \
-H "Content-Type: application/json" \
-d '{
"field1": "updated value"
}'
# Delete
curl -X DELETE http://localhost:8080/v1/{resources}/1
# Check response status
curl -i http://localhost:8080/v1/{resources}
Common Development Tasks
Update Dependencies:
# Update specific package
go get gitlab.cept.gov.in/it-2.0-common/n-api-server@latest
# Update all dependencies
go get -u ./...
# Tidy up
go mod tidy
Generate Validators:
# Install govalid (once)
go install github.com/twpayne/go-govalid/cmd/govalid@latest
# Generate validators
cd handler
govalid
Format Code:
# Format all files
go fmt ./...
# Or use gofmt
gofmt -w .
Lint Code:
# Install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Run linter
golangci-lint run
Run Tests:
# Run all tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run tests in specific package
go test ./handler/...
# Verbose output
go test -v ./...
Debugging Tips
Enable Debug Logging:
# In config.yaml
log:
level: debug # Change from info to debug
Check Database Connections:
# In psql, check active connections
SELECT * FROM pg_stat_activity WHERE datname = 'your_database';
View Server Logs:
# Logs are output to stdout by default
# Redirect to file:
go run main.go > app.log 2>&1
Common Issues:
-
Port already in use:
# Find process using port netstat -ano | findstr :8080 # Windows lsof -i :8080 # Linux/Mac # Kill process taskkill /PID <pid> /F # Windows kill -9 <pid> # Linux/Mac -
Database connection failed:
- Check config.yaml database credentials
- Ensure PostgreSQL is running
- Check network connectivity
- Verify database exists
-
Validation errors:
- Regenerate validators:
cd handler && govalid - Check validation tags in request structs
- Ensure all required fields are provided
- Regenerate validators:
-
Import errors:
- Run
go mod tidy - Check module path in go.mod
- Verify all imports use correct module paths
- Run
Production Deployment
Build for Production:
# Build with optimizations
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o app main.go
# Build for Windows
GOOS=windows GOARCH=amd64 go build -o app.exe main.go
Environment Variables:
# Set environment
export ENV=prod
# Set database password
export DB_PASSWORD=secret
# Set Redis password
export REDIS_PASSWORD=secret
Docker Deployment (if using Docker):
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o /app/main main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/configs ./configs
EXPOSE 8080
CMD ["./main"]
Health Check Endpoint:
# Check if server is running
curl http://localhost:8080/health
# Or use built-in health endpoint (if available)
curl http://localhost:8080/v1/health
Version Control
Git Workflow:
# Create feature branch
git checkout -b feature/{resource-name}
# Add changes
git add .
# Commit with meaningful message
git commit -m "feat: add {resource} CRUD endpoints"
# Push to remote
git push origin feature/{resource-name}
# After review, merge to main
Commit Message Convention:
feat:- New featurefix:- Bug fixdocs:- Documentation changesrefactor:- Code refactoringtest:- Add/update testschore:- Maintenance tasks
Monitoring and Observability
Tracing (if enabled):
# In config.yaml
trace:
enabled: true
processor:
type: "otlp-grpc"
options:
host: "localhost:4317"
Metrics:
- Database connection pool metrics
- Request latency
- Error rates
- Active requests
Logs:
- Structured logging with context
- Error logging with stack traces
- Request/response logging