├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── cmd └── server.go ├── docker-compose.yml ├── docs └── reference │ └── openapi.v1.yaml ├── go.mod └── pkg ├── application └── registry │ └── ping │ └── registry.go ├── context └── ping │ ├── responses │ └── ping.go │ └── routing │ └── endpoints.go └── infrastructure ├── app └── factory.go ├── config └── factory.go └── kernel └── app.go /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Go API" 2 | APP_VERSION="v1.0.0" 3 | 4 | # HTTP Response Content-Type Header - Success 5 | HTTP_CONTENT_TYPE="application/vnd.api+json" 6 | 7 | # HTTP Response Content-Type Header - Error 8 | HTTP_PROBLEM="application/problem+json" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | go.sum -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ./ /app 6 | 7 | RUN go mod download 8 | 9 | ENTRYPOINT go run cmd/server.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go API 2 | 3 | A simple Go API following concepts of Domain Driven Design for educational purposes. 4 | 5 | Open API specification can be found [here](docs/reference/openapi.v1.yaml) 6 | 7 | ## Installation 8 | 9 | Clone the repo 10 | 11 | ```bash 12 | $ git clone git@github.com:JustSteveKing/go-api.git 13 | ``` 14 | 15 | ```bash 16 | $ cd go-api 17 | ``` 18 | 19 | ## Running 20 | 21 | Once installed, simply spin up the docker container: 22 | 23 | ```bash 24 | $ docker-compose up -d --build 25 | ``` 26 | 27 | This will give you a single endpoint for now under: `http://localhost:8080/ping` which is a healthcheck URL, and will return the following: 28 | 29 | ```json 30 | { 31 | "message": "Service Online" 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/JustSteveKing/go-api/pkg/application/registry/ping" 5 | application "github.com/JustSteveKing/go-api/pkg/infrastructure/app" 6 | "github.com/JustSteveKing/go-api/pkg/infrastructure/kernel" 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | // init is invoked before main() 11 | func init() { 12 | // loads values from .env into the system 13 | if err := godotenv.Load(); err != nil { 14 | panic("No .env file found") 15 | } 16 | } 17 | 18 | func main() { 19 | // Create our application 20 | app := kernel.Boot() 21 | 22 | // Build our services 23 | ping.BuildPingService(app) 24 | 25 | // Run our Application in a coroutine 26 | go func() { 27 | app.Run() 28 | }() 29 | 30 | // Wait for termination signals and shut down gracefully 31 | application.WaitForShutdown(app) 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | networks: 4 | juststeveking: 5 | 6 | services: 7 | go-docker: 8 | build: 9 | context: . 10 | dockerfile: Dockerfile 11 | container_name: go_docker_api 12 | networks: 13 | - juststeveking 14 | ports: 15 | - "8080:8080" 16 | volumes: 17 | - ./:/app -------------------------------------------------------------------------------- /docs/reference/openapi.v1.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Go API Example 4 | version: '1.0' 5 | contact: 6 | name: Steve McDougall 7 | url: 'https://www.juststeveking.uk' 8 | email: juststevemcd@gmail.com 9 | description: A simple Go Lang API example built for education purposes. 10 | servers: 11 | - url: 'http://localhost:8080' 12 | description: Docker 13 | paths: 14 | /ping: 15 | get: 16 | summary: Ping Service 17 | tags: [] 18 | responses: 19 | '200': 20 | description: OK 21 | headers: 22 | Content-Type: 23 | schema: 24 | type: string 25 | default: application/vnd.api+json 26 | description: The Response content type should be application/vnd.api+json 27 | required: true 28 | content: 29 | application/json: 30 | schema: 31 | $ref: '#/components/schemas/Ping-Message' 32 | operationId: get-ping 33 | description: 'A simple endpoint to ensure the API is online and working as expected, will return a message stating that the service is online.' 34 | parameters: [] 35 | parameters: [] 36 | components: 37 | schemas: 38 | Ping-Message: 39 | title: Ping-Message 40 | type: object 41 | properties: 42 | message: 43 | type: string 44 | description: A simple online and working message from the API 45 | required: 46 | - message 47 | x-examples: 48 | Service Online: 49 | message: Service Online 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/JustSteveKing/go-api 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/JustSteveKing/go-api-problem v1.0.0 // indirect 7 | github.com/JustSteveKing/go-http-response v1.0.0 8 | github.com/google/uuid v1.1.1 9 | github.com/gorilla/handlers v1.4.2 10 | github.com/gorilla/mux v1.7.4 11 | github.com/joho/godotenv v1.3.0 12 | github.com/micro/go-micro/v2 v2.9.1 13 | github.com/pborman/uuid v1.2.0 14 | go.uber.org/zap v1.15.0 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/application/registry/ping/registry.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustSteveKing/go-api/pkg/context/ping/routing" 7 | "github.com/JustSteveKing/go-api/pkg/infrastructure/app" 8 | ) 9 | 10 | // BuildPingService is responsible for setting up the Ping Context for our Domain 11 | func BuildPingService(app *app.Application) { 12 | // Create our Handler 13 | handler := routing.NewHandler(app) 14 | 15 | // Create a sub router for this service 16 | router := app.Router.Methods(http.MethodGet).Subrouter() 17 | 18 | // Register our service routes 19 | router.HandleFunc("/ping", handler.Handle).Name("ping:handle") 20 | } 21 | -------------------------------------------------------------------------------- /pkg/context/ping/responses/ping.go: -------------------------------------------------------------------------------- 1 | package responses 2 | 3 | // Response is the Ping Response 4 | type Response struct { 5 | Message string `json:"message"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/context/ping/routing/endpoints.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustSteveKing/go-api/pkg/context/ping/responses" 7 | "github.com/JustSteveKing/go-api/pkg/infrastructure/app" 8 | responseFactory "github.com/JustSteveKing/go-http-response" 9 | ) 10 | 11 | // Handler is the http.Handler for this request 12 | type Handler struct { 13 | app *app.Application 14 | } 15 | 16 | // NewHandler will create a new Handler to handle this request 17 | func NewHandler(app *app.Application) *Handler { 18 | return &Handler{app} 19 | } 20 | 21 | // Handle will handle the incoming request 22 | func (handler *Handler) Handle(response http.ResponseWriter, request *http.Request) { 23 | handler.app.Logger.Info("Ping Handler Dispatched.") 24 | 25 | responseFactory.Send( 26 | response, 27 | http.StatusOK, 28 | &responses.Response{ 29 | Message: "Service Online", 30 | }, 31 | handler.app.Config.HTTP.Content, 32 | ) 33 | 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /pkg/infrastructure/app/factory.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/JustSteveKing/go-api/pkg/infrastructure/config" 13 | "github.com/gorilla/mux" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // Application is our general purpose Application struct 18 | type Application struct { 19 | Server *http.Server 20 | Router *mux.Router 21 | Logger *zap.Logger 22 | Config *config.Config 23 | } 24 | 25 | // Run will run the Application server 26 | func (app *Application) Run() { 27 | err := app.Server.ListenAndServe() 28 | 29 | if err != nil { 30 | fmt.Println(err) 31 | } 32 | } 33 | 34 | // WaitForShutdown is a graceful way to handle server shutdown events 35 | func WaitForShutdown(application *Application) { 36 | // Create a channel to listen for OS signals 37 | interruptChan := make(chan os.Signal, 1) 38 | signal.Notify(interruptChan, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM) 39 | 40 | // Block until we receive a signal to our channel 41 | <-interruptChan 42 | 43 | application.Logger.Info("Received shutdown signal, gracefully terminating") 44 | 45 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 46 | defer cancel() 47 | application.Server.Shutdown(ctx) 48 | os.Exit(0) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/infrastructure/config/factory.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "os" 4 | 5 | // AppConfig is the Application configuration struct 6 | type AppConfig struct { 7 | Name string 8 | Version string 9 | Token string 10 | } 11 | 12 | // HTTPConfig is the Application HTTP configuration 13 | type HTTPConfig struct { 14 | Content string 15 | Problem string 16 | } 17 | 18 | // Config is the Configuration struct 19 | type Config struct { 20 | App AppConfig 21 | HTTP HTTPConfig 22 | } 23 | 24 | // New returns a new Config Struct 25 | func New() *Config { 26 | return &Config{ 27 | App: AppConfig{ 28 | Name: env("APP_NAME", "Go App"), 29 | Version: env("APP_VERSION", "v1.0"), 30 | Token: env("APP_TOKEN", ""), 31 | }, 32 | HTTP: HTTPConfig{ 33 | Content: env("HTTP_CONTENT_TYPE", "application/json"), 34 | Problem: env("HTTP_PROBLEM", "application/problem+json"), 35 | }, 36 | } 37 | } 38 | 39 | // env is a simple helper function to read an environment variable or return a default value 40 | func env(key string, defaultValue string) string { 41 | if value, exists := os.LookupEnv(key); exists { 42 | return value 43 | } 44 | 45 | return defaultValue 46 | } 47 | -------------------------------------------------------------------------------- /pkg/infrastructure/kernel/app.go: -------------------------------------------------------------------------------- 1 | package kernel 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/JustSteveKing/go-api/pkg/infrastructure/app" 9 | "github.com/JustSteveKing/go-api/pkg/infrastructure/config" 10 | "github.com/google/uuid" 11 | gohandlers "github.com/gorilla/handlers" 12 | "github.com/gorilla/mux" 13 | "github.com/micro/go-micro/v2/util/log" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // Boot the Application 18 | func Boot() *app.Application { 19 | // Configuration 20 | config := bootConfig() 21 | 22 | // Router 23 | router := bootRouter() 24 | 25 | // CORS 26 | corsHandler := gohandlers.CORS(gohandlers.AllowedOrigins([]string{"*"})) 27 | 28 | // Logger 29 | logger := bootLogger() 30 | defer logger.Sync() // flushes buffer, if any 31 | 32 | // Create and return and Application 33 | return &app.Application{ 34 | Server: &http.Server{ 35 | Addr: ":8080", 36 | Handler: corsHandler(requestIDMiddleware(router)), 37 | IdleTimeout: 120 * time.Second, 38 | ReadTimeout: 1 * time.Second, 39 | WriteTimeout: 1 * time.Second, 40 | }, 41 | Router: router, 42 | Logger: logger, 43 | Config: config, 44 | } 45 | } 46 | 47 | func bootConfig() *config.Config { 48 | return config.New() 49 | } 50 | 51 | func bootRouter() *mux.Router { 52 | return mux.NewRouter() 53 | } 54 | 55 | func bootLogger() *zap.Logger { 56 | logger, logError := zap.NewProduction() 57 | if logError != nil { 58 | panic(logError) 59 | } 60 | 61 | return logger 62 | } 63 | 64 | // ContextKey is used for context.Context value. The value requires a key that is not primitive type. 65 | type ContextKey string 66 | 67 | // ContextKeyRequestID is the ContextKey for RequestID 68 | const ContextKeyRequestID ContextKey = "requestID" 69 | 70 | func requestIDMiddleware(next http.Handler) http.Handler { 71 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 | 73 | ctx := r.Context() 74 | 75 | id := uuid.New() 76 | 77 | ctx = context.WithValue(ctx, ContextKeyRequestID, id.String()) 78 | 79 | r = r.WithContext(ctx) 80 | 81 | log.Debugf("Incomming request %s %s %s %s", r.Method, r.RequestURI, r.RemoteAddr, id.String()) 82 | 83 | next.ServeHTTP(w, r) 84 | 85 | log.Debugf("Finished handling http req. %s", id.String()) 86 | }) 87 | } 88 | --------------------------------------------------------------------------------