├── .DS_Store ├── .air.toml ├── .gitignore ├── Dockerfile ├── LICENSE ├── cmd ├── .DS_Store └── server │ ├── .DS_Store │ ├── Makefile │ ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml │ └── main.go ├── config └── config.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── errors │ └── errors.go ├── handlers │ ├── books.go │ └── handlers.go ├── middlewares │ └── middlewares.go ├── model │ ├── base_model.go │ └── books.go ├── server.go └── storage │ ├── books.go │ ├── postgres_db.go │ └── storage.go ├── migrations └── 1_create_books_table.up.sql ├── pkg ├── httputils │ └── http_response.go └── logger │ └── logger.go └── readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sash20m/go-api-template/74ede58b45a3407c241e7d6eaee60348e2b66de0/.DS_Store -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | watch_dir = "./" 5 | 6 | exclude_dir = ["assets", "tmp", "vendor"] 7 | 8 | 9 | [build] 10 | args_bin = [] 11 | bin = "cmd/server/tmp/main" 12 | cmd = "cd cmd/server && go build -o ./tmp/main ." 13 | delay = 0 14 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 15 | exclude_file = [] 16 | exclude_regex = ["_test.go"] 17 | exclude_unchanged = false 18 | follow_symlink = false 19 | full_bin = "" 20 | include_dir = [] 21 | include_ext = ["go", "tpl", "tmpl", "html"] 22 | include_file = [] 23 | kill_delay = "0s" 24 | log = "build-errors.log" 25 | poll = false 26 | poll_interval = 0 27 | rerun = false 28 | rerun_delay = 500 29 | send_interrupt = false 30 | stop_on_error = false 31 | 32 | [color] 33 | app = "" 34 | build = "yellow" 35 | main = "magenta" 36 | runner = "green" 37 | watcher = "cyan" 38 | 39 | [log] 40 | main_only = false 41 | time = false 42 | 43 | [misc] 44 | clean_on_exit = false 45 | 46 | [screen] 47 | clear_on_rebuild = false 48 | keep_scroll = true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /*/*/tmp 3 | 4 | .env 5 | /logs/* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 AS build 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod ./ 6 | COPY go.sum ./ 7 | 8 | RUN go mod download 9 | 10 | COPY ./cmd/server/main.go ./cmd/server/main.go 11 | COPY ./cmd/server/docs/ ./cmd/server/docs/ 12 | COPY ./pkg/ ./pkg/ 13 | COPY ./internal/ ./internal/ 14 | COPY ./config/ ./config/ 15 | 16 | RUN CGO_ENABLED=0 go build -o ./server ./cmd/server/main.go 17 | 18 | # Stage 2 19 | FROM alpine:latest 20 | 21 | RUN apk --no-cache add ca-certificates 22 | 23 | WORKDIR /root/ 24 | 25 | COPY --from=0 /app/server ./ 26 | COPY ./migrations/ ./migrations/ 27 | COPY .env.prod .env.prod 28 | COPY .env .env 29 | COPY ./logs/ ./logs/ 30 | 31 | EXPOSE 8080 32 | 33 | CMD ["./server"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alex Matei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sash20m/go-api-template/74ede58b45a3407c241e7d6eaee60348e2b66de0/cmd/.DS_Store -------------------------------------------------------------------------------- /cmd/server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sash20m/go-api-template/74ede58b45a3407c241e7d6eaee60348e2b66de0/cmd/server/.DS_Store -------------------------------------------------------------------------------- /cmd/server/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | fmt: 4 | go fmt ./... 5 | .PHONY:fmt 6 | 7 | lint: fmt 8 | golint ./... 9 | .PHONY:lint 10 | 11 | vet: lint 12 | go vet ./... 13 | shadow ./... 14 | .PHONY:vet 15 | 16 | swag: vet 17 | swag init -d ./,../../internal 18 | .PHONY: swag 19 | 20 | build: swag 21 | go build main.go 22 | .PHONY:build -------------------------------------------------------------------------------- /cmd/server/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/api/book/add": { 19 | "post": { 20 | "consumes": [ 21 | "application/json" 22 | ], 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "tags": [ 27 | "Books" 28 | ], 29 | "summary": "Add a specific book", 30 | "parameters": [ 31 | { 32 | "description": "Book title", 33 | "name": "title", 34 | "in": "body", 35 | "required": true, 36 | "schema": { 37 | "type": "string" 38 | } 39 | }, 40 | { 41 | "description": "Book author", 42 | "name": "author", 43 | "in": "body", 44 | "required": true, 45 | "schema": { 46 | "type": "string" 47 | } 48 | }, 49 | { 50 | "description": "Book coverUrl", 51 | "name": "coverUrl", 52 | "in": "body", 53 | "required": true, 54 | "schema": { 55 | "type": "string" 56 | } 57 | }, 58 | { 59 | "description": "Book post url", 60 | "name": "postUrl", 61 | "in": "body", 62 | "required": true, 63 | "schema": { 64 | "type": "string" 65 | } 66 | } 67 | ], 68 | "responses": { 69 | "200": { 70 | "description": "OK", 71 | "schema": { 72 | "$ref": "#/definitions/model.IDResponse" 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "/api/book/delete/{id}": { 79 | "delete": { 80 | "produces": [ 81 | "application/json" 82 | ], 83 | "tags": [ 84 | "Books" 85 | ], 86 | "summary": "Delete a specific book", 87 | "parameters": [ 88 | { 89 | "type": "integer", 90 | "description": "Book ID", 91 | "name": "id", 92 | "in": "path", 93 | "required": true 94 | } 95 | ], 96 | "responses": { 97 | "200": { 98 | "description": "OK", 99 | "schema": { 100 | "$ref": "#/definitions/model.GetBookResponse" 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | "/api/book/update": { 107 | "patch": { 108 | "consumes": [ 109 | "application/json" 110 | ], 111 | "produces": [ 112 | "application/json" 113 | ], 114 | "tags": [ 115 | "Books" 116 | ], 117 | "summary": "Update a specific book", 118 | "parameters": [ 119 | { 120 | "description": "Book title", 121 | "name": "title", 122 | "in": "body", 123 | "schema": { 124 | "type": "string" 125 | } 126 | }, 127 | { 128 | "description": "Book author", 129 | "name": "author", 130 | "in": "body", 131 | "schema": { 132 | "type": "string" 133 | } 134 | }, 135 | { 136 | "description": "Book coverUrl", 137 | "name": "coverUrl", 138 | "in": "body", 139 | "schema": { 140 | "type": "string" 141 | } 142 | }, 143 | { 144 | "description": "Book post url", 145 | "name": "postUrl", 146 | "in": "body", 147 | "schema": { 148 | "type": "string" 149 | } 150 | } 151 | ], 152 | "responses": { 153 | "200": { 154 | "description": "OK", 155 | "schema": { 156 | "$ref": "#/definitions/model.IDResponse" 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "/api/book/{id}": { 163 | "get": { 164 | "produces": [ 165 | "application/json" 166 | ], 167 | "tags": [ 168 | "Books" 169 | ], 170 | "summary": "Get a specific book", 171 | "parameters": [ 172 | { 173 | "type": "integer", 174 | "description": "Book ID", 175 | "name": "id", 176 | "in": "path", 177 | "required": true 178 | } 179 | ], 180 | "responses": { 181 | "200": { 182 | "description": "OK", 183 | "schema": { 184 | "$ref": "#/definitions/model.GetBookResponse" 185 | } 186 | } 187 | } 188 | } 189 | }, 190 | "/api/books": { 191 | "get": { 192 | "produces": [ 193 | "application/json" 194 | ], 195 | "tags": [ 196 | "Books" 197 | ], 198 | "summary": "Get all books", 199 | "responses": { 200 | "200": { 201 | "description": "OK", 202 | "schema": { 203 | "type": "array", 204 | "items": { 205 | "$ref": "#/definitions/model.GetBookResponse" 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | }, 213 | "definitions": { 214 | "model.GetBookResponse": { 215 | "type": "object", 216 | "properties": { 217 | "author": { 218 | "type": "string" 219 | }, 220 | "coverUrl": { 221 | "type": "string" 222 | }, 223 | "id": { 224 | "type": "integer" 225 | }, 226 | "postUrl": { 227 | "type": "string" 228 | }, 229 | "title": { 230 | "type": "string" 231 | } 232 | } 233 | }, 234 | "model.IDResponse": { 235 | "type": "object", 236 | "properties": { 237 | "id": { 238 | "type": "integer" 239 | } 240 | } 241 | } 242 | } 243 | }` 244 | 245 | // SwaggerInfo holds exported Swagger Info so clients can modify it 246 | var SwaggerInfo = &swag.Spec{ 247 | Version: "", 248 | Host: "", 249 | BasePath: "", 250 | Schemes: []string{}, 251 | Title: "Go Rest Api", 252 | Description: "Api Endpoints for Go Server", 253 | InfoInstanceName: "swagger", 254 | SwaggerTemplate: docTemplate, 255 | LeftDelim: "{{", 256 | RightDelim: "}}", 257 | } 258 | 259 | func init() { 260 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 261 | } 262 | -------------------------------------------------------------------------------- /cmd/server/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Api Endpoints for Go Server", 5 | "title": "Go Rest Api", 6 | "contact": {} 7 | }, 8 | "paths": { 9 | "/api/book/add": { 10 | "post": { 11 | "consumes": [ 12 | "application/json" 13 | ], 14 | "produces": [ 15 | "application/json" 16 | ], 17 | "tags": [ 18 | "Books" 19 | ], 20 | "summary": "Add a specific book", 21 | "parameters": [ 22 | { 23 | "description": "Book title", 24 | "name": "title", 25 | "in": "body", 26 | "required": true, 27 | "schema": { 28 | "type": "string" 29 | } 30 | }, 31 | { 32 | "description": "Book author", 33 | "name": "author", 34 | "in": "body", 35 | "required": true, 36 | "schema": { 37 | "type": "string" 38 | } 39 | }, 40 | { 41 | "description": "Book coverUrl", 42 | "name": "coverUrl", 43 | "in": "body", 44 | "required": true, 45 | "schema": { 46 | "type": "string" 47 | } 48 | }, 49 | { 50 | "description": "Book post url", 51 | "name": "postUrl", 52 | "in": "body", 53 | "required": true, 54 | "schema": { 55 | "type": "string" 56 | } 57 | } 58 | ], 59 | "responses": { 60 | "200": { 61 | "description": "OK", 62 | "schema": { 63 | "$ref": "#/definitions/model.IDResponse" 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | "/api/book/delete/{id}": { 70 | "delete": { 71 | "produces": [ 72 | "application/json" 73 | ], 74 | "tags": [ 75 | "Books" 76 | ], 77 | "summary": "Delete a specific book", 78 | "parameters": [ 79 | { 80 | "type": "integer", 81 | "description": "Book ID", 82 | "name": "id", 83 | "in": "path", 84 | "required": true 85 | } 86 | ], 87 | "responses": { 88 | "200": { 89 | "description": "OK", 90 | "schema": { 91 | "$ref": "#/definitions/model.GetBookResponse" 92 | } 93 | } 94 | } 95 | } 96 | }, 97 | "/api/book/update": { 98 | "patch": { 99 | "consumes": [ 100 | "application/json" 101 | ], 102 | "produces": [ 103 | "application/json" 104 | ], 105 | "tags": [ 106 | "Books" 107 | ], 108 | "summary": "Update a specific book", 109 | "parameters": [ 110 | { 111 | "description": "Book title", 112 | "name": "title", 113 | "in": "body", 114 | "schema": { 115 | "type": "string" 116 | } 117 | }, 118 | { 119 | "description": "Book author", 120 | "name": "author", 121 | "in": "body", 122 | "schema": { 123 | "type": "string" 124 | } 125 | }, 126 | { 127 | "description": "Book coverUrl", 128 | "name": "coverUrl", 129 | "in": "body", 130 | "schema": { 131 | "type": "string" 132 | } 133 | }, 134 | { 135 | "description": "Book post url", 136 | "name": "postUrl", 137 | "in": "body", 138 | "schema": { 139 | "type": "string" 140 | } 141 | } 142 | ], 143 | "responses": { 144 | "200": { 145 | "description": "OK", 146 | "schema": { 147 | "$ref": "#/definitions/model.IDResponse" 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "/api/book/{id}": { 154 | "get": { 155 | "produces": [ 156 | "application/json" 157 | ], 158 | "tags": [ 159 | "Books" 160 | ], 161 | "summary": "Get a specific book", 162 | "parameters": [ 163 | { 164 | "type": "integer", 165 | "description": "Book ID", 166 | "name": "id", 167 | "in": "path", 168 | "required": true 169 | } 170 | ], 171 | "responses": { 172 | "200": { 173 | "description": "OK", 174 | "schema": { 175 | "$ref": "#/definitions/model.GetBookResponse" 176 | } 177 | } 178 | } 179 | } 180 | }, 181 | "/api/books": { 182 | "get": { 183 | "produces": [ 184 | "application/json" 185 | ], 186 | "tags": [ 187 | "Books" 188 | ], 189 | "summary": "Get all books", 190 | "responses": { 191 | "200": { 192 | "description": "OK", 193 | "schema": { 194 | "type": "array", 195 | "items": { 196 | "$ref": "#/definitions/model.GetBookResponse" 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | }, 204 | "definitions": { 205 | "model.GetBookResponse": { 206 | "type": "object", 207 | "properties": { 208 | "author": { 209 | "type": "string" 210 | }, 211 | "coverUrl": { 212 | "type": "string" 213 | }, 214 | "id": { 215 | "type": "integer" 216 | }, 217 | "postUrl": { 218 | "type": "string" 219 | }, 220 | "title": { 221 | "type": "string" 222 | } 223 | } 224 | }, 225 | "model.IDResponse": { 226 | "type": "object", 227 | "properties": { 228 | "id": { 229 | "type": "integer" 230 | } 231 | } 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /cmd/server/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | model.GetBookResponse: 3 | properties: 4 | author: 5 | type: string 6 | coverUrl: 7 | type: string 8 | id: 9 | type: integer 10 | postUrl: 11 | type: string 12 | title: 13 | type: string 14 | type: object 15 | model.IDResponse: 16 | properties: 17 | id: 18 | type: integer 19 | type: object 20 | info: 21 | contact: {} 22 | description: Api Endpoints for Go Server 23 | title: Go Rest Api 24 | paths: 25 | /api/book/{id}: 26 | get: 27 | parameters: 28 | - description: Book ID 29 | in: path 30 | name: id 31 | required: true 32 | type: integer 33 | produces: 34 | - application/json 35 | responses: 36 | "200": 37 | description: OK 38 | schema: 39 | $ref: '#/definitions/model.GetBookResponse' 40 | summary: Get a specific book 41 | tags: 42 | - Books 43 | /api/book/add: 44 | post: 45 | consumes: 46 | - application/json 47 | parameters: 48 | - description: Book title 49 | in: body 50 | name: title 51 | required: true 52 | schema: 53 | type: string 54 | - description: Book author 55 | in: body 56 | name: author 57 | required: true 58 | schema: 59 | type: string 60 | - description: Book coverUrl 61 | in: body 62 | name: coverUrl 63 | required: true 64 | schema: 65 | type: string 66 | - description: Book post url 67 | in: body 68 | name: postUrl 69 | required: true 70 | schema: 71 | type: string 72 | produces: 73 | - application/json 74 | responses: 75 | "200": 76 | description: OK 77 | schema: 78 | $ref: '#/definitions/model.IDResponse' 79 | summary: Add a specific book 80 | tags: 81 | - Books 82 | /api/book/delete/{id}: 83 | delete: 84 | parameters: 85 | - description: Book ID 86 | in: path 87 | name: id 88 | required: true 89 | type: integer 90 | produces: 91 | - application/json 92 | responses: 93 | "200": 94 | description: OK 95 | schema: 96 | $ref: '#/definitions/model.GetBookResponse' 97 | summary: Delete a specific book 98 | tags: 99 | - Books 100 | /api/book/update: 101 | patch: 102 | consumes: 103 | - application/json 104 | parameters: 105 | - description: Book title 106 | in: body 107 | name: title 108 | schema: 109 | type: string 110 | - description: Book author 111 | in: body 112 | name: author 113 | schema: 114 | type: string 115 | - description: Book coverUrl 116 | in: body 117 | name: coverUrl 118 | schema: 119 | type: string 120 | - description: Book post url 121 | in: body 122 | name: postUrl 123 | schema: 124 | type: string 125 | produces: 126 | - application/json 127 | responses: 128 | "200": 129 | description: OK 130 | schema: 131 | $ref: '#/definitions/model.IDResponse' 132 | summary: Update a specific book 133 | tags: 134 | - Books 135 | /api/books: 136 | get: 137 | produces: 138 | - application/json 139 | responses: 140 | "200": 141 | description: OK 142 | schema: 143 | items: 144 | $ref: '#/definitions/model.GetBookResponse' 145 | type: array 146 | summary: Get all books 147 | tags: 148 | - Books 149 | swagger: "2.0" 150 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/joho/godotenv" 9 | "github.com/sash20m/go-api-template/config" 10 | api "github.com/sash20m/go-api-template/internal" 11 | "github.com/sash20m/go-api-template/pkg/logger" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // @title Go Rest Api 16 | // @description Api Endpoints for Go Server 17 | func main() { 18 | err := godotenv.Load() 19 | if err != nil { 20 | logger.Log.WithFields(logrus.Fields{ 21 | "err": err, 22 | }).Error("Can't load config from .env. Problem with .env, or the server is in production environment.") 23 | return 24 | } 25 | 26 | config := config.ApiEnvConfig{ 27 | Env: strings.ToUpper(os.Getenv("ENV")), 28 | Port: os.Getenv("PORT"), 29 | Version: os.Getenv("VERSION"), 30 | } 31 | 32 | logger.Log.WithFields(logrus.Fields{ 33 | "env": config.Env, 34 | "version": config.Version, 35 | "port": config.Port, 36 | }).Info("Loaded app config") 37 | 38 | var wg sync.WaitGroup 39 | wg.Add(1) 40 | 41 | // Starting our magnificent server 42 | go func() { 43 | defer wg.Done() 44 | 45 | server := api.AppServer{} 46 | defer func() { 47 | if r := recover(); r != nil { 48 | server.OnShutdown() 49 | } 50 | }() 51 | 52 | server.Run(config) 53 | }() 54 | wg.Wait() 55 | 56 | } 57 | 58 | // cSpell:ignore logrus godotenv 59 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ApiEnvConfig struct { 4 | // LOCAL, DEV, STG, PRD 5 | Env string 6 | // server traffic on this port 7 | Port string 8 | // path to VERSION file 9 | Version string 10 | } 11 | 12 | const DEV_ENV = "DEV" 13 | const STAGE_ENV = "STAGE" 14 | const PROD_ENV = "PROD" 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | api: 5 | container_name: api 6 | build: . 7 | image: api:go 8 | env_file: 9 | - .env 10 | environment: 11 | DB_USER: ${DB_USER} 12 | DB_NAME: ${DB_NAME} 13 | DB_PASSWORD: ${DB_PASSWORD} 14 | DB_HOST: ${DB_HOST} 15 | ports: 16 | - "8080:8080" 17 | depends_on: 18 | - postgres 19 | postgres: 20 | container_name: postgres 21 | image: postgres:16-alpine 22 | env_file: 23 | - .env 24 | environment: 25 | POSTGRES_USER: ${DB_USER} 26 | POSTGRES_DB: ${DB_NAME} 27 | POSTGRES_PASSWORD: ${DB_PASSWORD} 28 | ports: 29 | - "5432:5432" 30 | volumes: 31 | - pgdata:/var/lib/postgresql/data 32 | 33 | volumes: 34 | pgdata: {} 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sash20m/go-api-template 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.15.5 7 | github.com/golang-migrate/migrate v3.5.4+incompatible 8 | github.com/google/uuid v1.4.0 9 | github.com/gorilla/mux v1.8.0 10 | github.com/jmoiron/sqlx v1.3.5 11 | github.com/joho/godotenv v1.5.1 12 | github.com/lib/pq v1.10.9 13 | github.com/rs/cors v1.10.1 14 | github.com/sirupsen/logrus v1.9.3 15 | github.com/swaggo/http-swagger/v2 v2.0.2 16 | github.com/swaggo/swag v1.16.2 17 | github.com/unrolled/render v1.6.0 18 | github.com/unrolled/secure v1.13.0 19 | github.com/urfave/negroni v1.0.0 20 | ) 21 | 22 | require ( 23 | github.com/KyleBanks/depth v1.2.1 // indirect 24 | github.com/Microsoft/go-winio v0.6.1 // indirect 25 | github.com/distribution/reference v0.5.0 // indirect 26 | github.com/docker/distribution v2.8.3+incompatible // indirect 27 | github.com/docker/docker v24.0.7+incompatible // indirect 28 | github.com/docker/go-connections v0.4.0 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/fsnotify/fsnotify v1.6.0 // indirect 31 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 32 | github.com/go-openapi/jsonpointer v0.20.0 // indirect 33 | github.com/go-openapi/jsonreference v0.20.2 // indirect 34 | github.com/go-openapi/spec v0.20.9 // indirect 35 | github.com/go-openapi/swag v0.22.4 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/leodido/go-urn v1.2.4 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/opencontainers/go-digest v1.0.0 // indirect 43 | github.com/opencontainers/image-spec v1.0.2 // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/rogpeppe/go-internal v1.11.0 // indirect 46 | github.com/swaggo/files/v2 v2.0.0 // indirect 47 | golang.org/x/crypto v0.15.0 // indirect 48 | golang.org/x/net v0.18.0 // indirect 49 | golang.org/x/sys v0.14.0 // indirect 50 | golang.org/x/text v0.14.0 // indirect 51 | golang.org/x/tools v0.15.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 4 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= 10 | github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 11 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 12 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 13 | github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= 14 | github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 15 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 16 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 17 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 18 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 19 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 20 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 21 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 22 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 23 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 24 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 25 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 26 | github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= 27 | github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= 28 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 29 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 30 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 31 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 32 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 33 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 34 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 35 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 36 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 37 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 38 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 39 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 40 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 41 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 42 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 43 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 44 | github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= 45 | github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 46 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 47 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 48 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 49 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 50 | github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= 51 | github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= 52 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 53 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 54 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 55 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 56 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 57 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 58 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 59 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 60 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 61 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 62 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 63 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 66 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 67 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 68 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 69 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 70 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 71 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 72 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 73 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 74 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 75 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 76 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 77 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 78 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 79 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 80 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 81 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 82 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 83 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 84 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 85 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 86 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 87 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 88 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 89 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 90 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 91 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 92 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 93 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 94 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 95 | github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= 96 | github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 97 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 98 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 99 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 101 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 102 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 103 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 104 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 105 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 106 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 107 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 108 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 109 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 110 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 111 | github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= 112 | github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= 113 | github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= 114 | github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= 115 | github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= 116 | github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= 117 | github.com/unrolled/render v1.6.0 h1:CMhr7HKRAzVI1RltKSo8JMRaokFi60ObV9I5uSxETJE= 118 | github.com/unrolled/render v1.6.0/go.mod h1:NoaP3JGGHcYDAqu6gTDz01E2TMqBybJ8dpR6qqRBVPQ= 119 | github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= 120 | github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= 121 | github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= 122 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 123 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 124 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 125 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 126 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 127 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 128 | golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= 129 | golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 130 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 131 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 132 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 133 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 134 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 135 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 136 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 137 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 138 | golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= 139 | golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= 140 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 149 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 150 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 151 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 153 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 156 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 157 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 158 | golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= 159 | golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= 160 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 168 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 169 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 171 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 172 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 173 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 174 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 175 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 176 | -------------------------------------------------------------------------------- /internal/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // Used for custom errors 4 | type Err struct { 5 | Message string `json:"message"` 6 | Data any `json:"data,omitempty"` 7 | } 8 | 9 | func (err Err) Error() string { 10 | return err.Message 11 | } 12 | 13 | // Sentinel errors 14 | const ErrResourceUnavailable = "This resource is unavailable" 15 | -------------------------------------------------------------------------------- /internal/handlers/books.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/go-playground/validator/v10" 11 | "github.com/gorilla/mux" 12 | "github.com/sash20m/go-api-template/internal/model" 13 | "github.com/sash20m/go-api-template/pkg/logger" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // GetBooks godoc 18 | // 19 | // @Summary Get all books 20 | // @Tags Books 21 | // @Produce json 22 | // @Success 200 {object} []model.GetBookResponse 23 | // 24 | // @Router /api/books [get] 25 | func (h *Handlers) GetBooksHandler(w http.ResponseWriter, r *http.Request) { 26 | ctx := r.Context() 27 | 28 | books, err := h.Storage.GetBooks(ctx) 29 | if err != nil { 30 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 31 | return 32 | } 33 | 34 | bookResponse := []model.GetBookResponse{} 35 | for _, book := range books { 36 | bookResponse = append(bookResponse, model.GetBookResponse{ 37 | ID: book.ID, 38 | Title: book.Title, 39 | Author: book.Author, 40 | CoverURL: book.CoverURL, 41 | PostURL: fmt.Sprint(book.PostURL), 42 | }) 43 | 44 | } 45 | 46 | err = h.Sender.JSON(w, http.StatusOK, bookResponse) 47 | if err != nil { 48 | logger.OutputLog.WithFields(logrus.Fields{ 49 | "err": err.Error(), 50 | }).Fatal("Error when requesting /books") 51 | 52 | panic(err) 53 | } 54 | } 55 | 56 | // GetBook godoc 57 | // 58 | // @Summary Get a specific book 59 | // @Tags Books 60 | // @Produce json 61 | // @Param id path int true "Book ID" 62 | // @Success 200 {object} model.GetBookResponse 63 | // 64 | // @Router /api/book/{id} [get] 65 | func (h *Handlers) GetBookHandler(w http.ResponseWriter, r *http.Request) { 66 | ctx := r.Context() 67 | 68 | vars := mux.Vars(r) 69 | id, err := strconv.Atoi(vars["id"]) 70 | if err != nil { 71 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 72 | return 73 | } 74 | 75 | var book model.Book 76 | book, err = h.Storage.GetBook(ctx, id) 77 | if err != nil { 78 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 79 | return 80 | } 81 | 82 | if (model.Book{}) == book { 83 | h.Sender.JSON(w, http.StatusBadRequest, "Book with id="+fmt.Sprint(book.ID)+" not found") 84 | if err != nil { 85 | panic(err) 86 | } 87 | return 88 | } 89 | 90 | bookResponse := model.GetBookResponse{ 91 | ID: book.ID, 92 | Title: book.Title, 93 | Author: book.Author, 94 | CoverURL: book.CoverURL, 95 | PostURL: book.PostURL.String, 96 | } 97 | 98 | err = h.Sender.JSON(w, http.StatusOK, bookResponse) 99 | if err != nil { 100 | logger.OutputLog.WithFields(logrus.Fields{ 101 | "err": err.Error(), 102 | }).Fatal(fmt.Sprint("Error when requesting /book/", book.ID)) 103 | 104 | panic(err) 105 | } 106 | } 107 | 108 | // AddBook godoc 109 | // 110 | // @Summary Add a specific book 111 | // @Tags Books 112 | // @Produce json 113 | // @Accept json 114 | // @Param title body string true "Book title" 115 | // @Param author body string true "Book author" 116 | // @Param coverUrl body string true "Book coverUrl" 117 | // @Param postUrl body string true "Book post url" 118 | // @Success 200 {object} model.IDResponse 119 | // 120 | // @Router /api/book/add [post] 121 | func (h *Handlers) AddBookHandler(w http.ResponseWriter, r *http.Request) { 122 | ctx := r.Context() 123 | var book model.AddBookRequest 124 | 125 | err := json.NewDecoder(r.Body).Decode(&book) 126 | if err != nil { 127 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 128 | return 129 | } 130 | 131 | err = Validate.Struct(book) 132 | if err != nil { 133 | var errs []string 134 | for _, err := range err.(validator.ValidationErrors) { 135 | errs = append(errs, err.Field()+" "+err.Tag()) 136 | } 137 | h.Sender.JSON(w, http.StatusBadRequest, strings.Join(errs, ", ")) 138 | return 139 | } 140 | 141 | id, err := h.Storage.AddBook(ctx, book) 142 | if err != nil { 143 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 144 | return 145 | } 146 | 147 | response := model.IDResponse{ID: id} 148 | err = h.Sender.JSON(w, http.StatusOK, response) 149 | if err != nil { 150 | panic(err) 151 | } 152 | } 153 | 154 | // UpdateBook godoc 155 | // 156 | // @Summary Update a specific book 157 | // @Tags Books 158 | // @Produce json 159 | // @Accept json 160 | // @Param title body string false "Book title" 161 | // @Param author body string false "Book author" 162 | // @Param coverUrl body string false "Book coverUrl" 163 | // @Param postUrl body string false "Book post url" 164 | // @Success 200 {object} model.IDResponse 165 | // 166 | // @Router /api/book/update [patch] 167 | func (h *Handlers) UpdateBookHandler(w http.ResponseWriter, r *http.Request) { 168 | ctx := r.Context() 169 | var book model.UpdateBookRequest 170 | 171 | err := json.NewDecoder(r.Body).Decode(&book) 172 | if err != nil { 173 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 174 | return 175 | } 176 | 177 | err = Validate.Struct(book) 178 | if err != nil { 179 | var errs []string 180 | for _, err := range err.(validator.ValidationErrors) { 181 | errs = append(errs, err.Field()+" "+err.Tag()) 182 | } 183 | h.Sender.JSON(w, http.StatusBadRequest, strings.Join(errs, ", ")) 184 | return 185 | } 186 | 187 | exists, err := h.Storage.VerifyBookExists(ctx, book.ID) 188 | if err != nil { 189 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 190 | return 191 | } 192 | 193 | if !exists { 194 | h.Sender.JSON(w, http.StatusBadRequest, "Book with id="+fmt.Sprint(book.ID)+" not found") 195 | return 196 | } 197 | 198 | id, err := h.Storage.UpdateBook(ctx, book) 199 | if err != nil { 200 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 201 | return 202 | } 203 | 204 | response := model.IDResponse{ID: id} 205 | err = h.Sender.JSON(w, http.StatusOK, response) 206 | if err != nil { 207 | panic(err) 208 | } 209 | } 210 | 211 | // DeleteBook godoc 212 | // 213 | // @Summary Delete a specific book 214 | // @Tags Books 215 | // @Produce json 216 | // @Param id path int true "Book ID" 217 | // @Success 200 {object} model.GetBookResponse 218 | // 219 | // @Router /api/book/delete/{id} [delete] 220 | func (h *Handlers) DeleteBookHandler(w http.ResponseWriter, r *http.Request) { 221 | ctx := r.Context() 222 | 223 | vars := mux.Vars(r) 224 | id, err := strconv.Atoi(vars["id"]) 225 | if err != nil { 226 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 227 | return 228 | } 229 | 230 | exists, err := h.Storage.VerifyBookExists(ctx, id) 231 | if err != nil { 232 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 233 | return 234 | } 235 | 236 | if !exists { 237 | err = h.Sender.JSON(w, http.StatusBadRequest, "Book with id="+fmt.Sprint(id)+" not found") 238 | if err != nil { 239 | panic(err) 240 | } 241 | return 242 | } 243 | 244 | err = h.Storage.DeleteBook(ctx, id) 245 | if err != nil { 246 | h.Sender.JSON(w, http.StatusInternalServerError, err.Error()) 247 | return 248 | } 249 | 250 | err = h.Sender.JSON(w, http.StatusOK, map[string]bool{"success": true}) 251 | if err != nil { 252 | panic(err) 253 | } 254 | } 255 | 256 | // cSpell:ignore godoc logrus 257 | -------------------------------------------------------------------------------- /internal/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/sash20m/go-api-template/internal/storage" 6 | "github.com/sash20m/go-api-template/pkg/httputils" 7 | ) 8 | 9 | // Handlers implements all the handler functions and has the dependencies that they use (Sender, Storage). 10 | type Handlers struct { 11 | Sender *httputils.Sender 12 | Storage storage.StorageInterface 13 | } 14 | 15 | // Validate is a singleton that provides validation services for in handlers. 16 | var Validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) 17 | -------------------------------------------------------------------------------- /internal/middlewares/middlewares.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // TrackRequestMiddleware adds an unique id to each request for it to be tracked 11 | // afterwards in logs if needed. 12 | func TrackRequestMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 13 | ctx := r.Context() 14 | 15 | requestID := uuid.New().String() 16 | ctx = context.WithValue(ctx, "requestID", requestID) 17 | 18 | r = r.WithContext(ctx) 19 | 20 | next(w, r) 21 | } 22 | -------------------------------------------------------------------------------- /internal/model/base_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // Base creates the default model that every other model is based on. 6 | type Base struct { 7 | CreatedAt time.Time `json:"createdAt" db:"created_at"` 8 | UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/model/books.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "database/sql" 4 | 5 | // Book is the db schema for the books table 6 | type Book struct { 7 | ID int `json:"id" db:"id"` 8 | Title string `json:"title" db:"title"` 9 | Author string `json:"author" db:"author"` 10 | CoverURL string `json:"coverUrl" db:"cover_url"` 11 | PostURL sql.NullString `json:"postUrl" db:"post_url"` 12 | Base 13 | } 14 | 15 | // Below are the structures of the request/response structs in the books handler. 16 | 17 | type AddBookRequest struct { 18 | Title string `json:"title" validate:"required"` 19 | Author string `json:"author" validate:"required"` 20 | CoverURL string `json:"coverUrl" validate:"required"` 21 | PostURL string `json:"postUrl" validate:"required"` 22 | } 23 | 24 | type GetBookResponse struct { 25 | ID int `json:"id"` 26 | Title string `json:"title"` 27 | Author string `json:"author"` 28 | CoverURL string `json:"coverUrl"` 29 | PostURL string `json:"postUrl"` 30 | } 31 | 32 | type UpdateBookRequest struct { 33 | ID int `json:"id" validate:"required"` 34 | Title string `json:"title"` 35 | Author string `json:"author"` 36 | CoverURL string `json:"coverUrl"` 37 | PostURL string `json:"postUrl"` 38 | } 39 | 40 | type IDResponse struct { 41 | ID int `json:"id"` 42 | } 43 | -------------------------------------------------------------------------------- /internal/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/rs/cors" 10 | _ "github.com/sash20m/go-api-template/cmd/server/docs" 11 | "github.com/sash20m/go-api-template/config" 12 | "github.com/sash20m/go-api-template/internal/handlers" 13 | "github.com/sash20m/go-api-template/internal/middlewares" 14 | "github.com/sash20m/go-api-template/internal/storage" 15 | "github.com/sash20m/go-api-template/pkg/httputils" 16 | "github.com/sash20m/go-api-template/pkg/logger" 17 | httpSwagger "github.com/swaggo/http-swagger/v2" 18 | "github.com/unrolled/render" 19 | "github.com/unrolled/secure" 20 | 21 | "github.com/urfave/negroni" 22 | ) 23 | 24 | type AppServer struct { 25 | Env string 26 | Port string 27 | Version string 28 | handlers.Handlers 29 | } 30 | 31 | func (app *AppServer) Run(appConfig config.ApiEnvConfig) { 32 | app.Env = appConfig.Env 33 | app.Port = appConfig.Port 34 | app.Version = appConfig.Version 35 | app.Sender = &httputils.Sender{ 36 | Render: render.New(render.Options{ 37 | IndentJSON: true, 38 | }), 39 | } 40 | 41 | // can change DB implementation from here 42 | storage, err := storage.NewPostgresDB() 43 | if err != nil { 44 | logger.Log.Error(err) 45 | panic(err.Error()) 46 | } 47 | // Migrations which will update the DB or create it if it doesn't exist. 48 | if err := storage.MigratePostgres("file://migrations"); err != nil { 49 | logger.Log.Fatal(err) 50 | } 51 | app.Storage = storage 52 | 53 | router := mux.NewRouter().StrictSlash(true) 54 | router.MethodNotAllowedHandler = http.HandlerFunc(app.NotAllowedHandler) 55 | router.NotFoundHandler = http.HandlerFunc(app.NotFoundHandler) 56 | router.Methods("GET").Path("/api/books").HandlerFunc(app.GetBooksHandler) 57 | router.Methods("GET").Path("/api/book/{id:[0-9]+}").HandlerFunc(app.GetBookHandler) 58 | router.Methods("POST").Path("/api/book/add").HandlerFunc(app.AddBookHandler) 59 | router.Methods("PATCH").Path("/api/book/update").HandlerFunc(app.UpdateBookHandler) 60 | router.Methods("DELETE").Path("/api/book/delete/{id:[0-9]+}").HandlerFunc(app.DeleteBookHandler) 61 | // other handlers 62 | 63 | if app.Env != config.PROD_ENV { 64 | router.Methods("GET").PathPrefix("/api/docs/").Handler(httpSwagger.Handler( 65 | httpSwagger.URL(fmt.Sprint("http://localhost:", app.Port, "/api/docs/doc.json")), 66 | httpSwagger.DeepLinking(true), 67 | httpSwagger.DocExpansion("none"), 68 | httpSwagger.DomID("swagger-ui"), 69 | )) 70 | } 71 | 72 | // Security Middlewares 73 | secureMiddleware := secure.New(secure.Options{ 74 | IsDevelopment: app.Env == "DEV", 75 | ContentTypeNosniff: true, 76 | SSLRedirect: true, 77 | // If the app is behind a proxy 78 | // SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, 79 | }) 80 | 81 | // Usual Middlewares 82 | n := negroni.New() 83 | n.Use(negroni.NewLogger()) 84 | n.Use(negroni.NewRecovery()) 85 | n.Use(negroni.HandlerFunc(secureMiddleware.HandlerFuncWithNext)) 86 | n.Use(negroni.HandlerFunc(middlewares.TrackRequestMiddleware)) 87 | corsMiddleware := cors.New(cors.Options{ 88 | AllowedOrigins: []string{"*"}, // Allows all origins 89 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 90 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, 91 | AllowCredentials: true, 92 | MaxAge: 86400, 93 | }) 94 | // router with cors middleware 95 | wrappedRouter := corsMiddleware.Handler(router) 96 | n.UseHandler(wrappedRouter) 97 | 98 | startupMessage := "Starting API server (v" + app.Version + ")" 99 | startupMessage = startupMessage + " on port " + app.Port 100 | startupMessage = startupMessage + " in " + app.Env + " mode." 101 | logger.Log.Info(startupMessage) 102 | 103 | addr := ":" + app.Port 104 | if app.Env == "DEV" { 105 | addr = "localhost:" + app.Port 106 | } 107 | 108 | server := http.Server{ 109 | Addr: addr, 110 | ReadTimeout: 30 * time.Second, 111 | WriteTimeout: 90 * time.Second, 112 | IdleTimeout: 120 * time.Second, 113 | Handler: n, 114 | } 115 | 116 | logger.Log.Info("Listening...") 117 | 118 | server.ListenAndServe() 119 | } 120 | 121 | // OnShutdown is called when the server has a panic. 122 | func (app *AppServer) OnShutdown() { 123 | // Do cleanup or logging 124 | logger.OutputLog.Error("Executed OnShutdown") 125 | } 126 | 127 | // Special server handlers, outside of specific routes we have 128 | func (app *AppServer) NotFoundHandler(w http.ResponseWriter, r *http.Request) { 129 | err := app.Sender.JSON(w, http.StatusNotFound, fmt.Sprint("Not Found ", r.URL)) 130 | if err != nil { 131 | panic(err) 132 | } 133 | } 134 | 135 | func (app *AppServer) NotAllowedHandler(w http.ResponseWriter, r *http.Request) { 136 | err := app.Sender.JSON(w, http.StatusMethodNotAllowed, fmt.Sprint(r.Method, " method not allowed")) 137 | if err != nil { 138 | panic(err) 139 | } 140 | } 141 | 142 | // cSpell:ignore negroni httputils Nosniff urfave sirupsen logrus 143 | -------------------------------------------------------------------------------- /internal/storage/books.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/sash20m/go-api-template/internal/model" 11 | ) 12 | 13 | func (s *Storage) AddBook(ctx context.Context, book model.AddBookRequest) (int, error) { 14 | var id int 15 | err := s.db.Get(&id, `INSERT INTO books(title, author, cover_url, post_url, created_at, updated_at) 16 | VALUES($1,$2,$3,$4,$5,$6) RETURNING id`, book.Title, book.Author, book.CoverURL, book.PostURL, time.Now().UTC(), time.Now().UTC()) 17 | 18 | if err != nil { 19 | return 0, err 20 | } 21 | 22 | return id, nil 23 | } 24 | 25 | func (s *Storage) GetBook(ctx context.Context, id int) (model.Book, error) { 26 | var book model.Book 27 | 28 | err := s.db.Get(&book, `Select * from books where id=$1`, id) 29 | if err != nil { 30 | return book, err 31 | } 32 | 33 | return book, nil 34 | } 35 | 36 | func (s *Storage) GetBooks(ctx context.Context) ([]model.Book, error) { 37 | var books []model.Book 38 | err := s.db.Select(&books, `SELECT * from books`) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return books, nil 44 | } 45 | 46 | func (s *Storage) UpdateBook(ctx context.Context, book model.UpdateBookRequest) (int, error) { 47 | var columns []string 48 | var argCount = 1 49 | var args []interface{} 50 | 51 | if book.Title != "" { 52 | columns = append(columns, fmt.Sprintf("title=$%d", argCount)) 53 | args = append(args, book.Title) 54 | argCount++ 55 | } 56 | 57 | if book.Author != "" { 58 | columns = append(columns, fmt.Sprintf("author=$%d", argCount)) 59 | args = append(args, book.Author) 60 | argCount++ 61 | } 62 | 63 | if book.CoverURL != "" { 64 | columns = append(columns, fmt.Sprintf("cover_url=$%d", argCount)) 65 | args = append(args, book.CoverURL) 66 | argCount++ 67 | } 68 | 69 | if book.PostURL != "" { 70 | columns = append(columns, fmt.Sprintf("post_url=$%d", argCount)) 71 | args = append(args, book.PostURL) 72 | argCount++ 73 | } 74 | 75 | columns = append(columns, fmt.Sprintf("updated_at=$%d", argCount)) 76 | args = append(args, time.Now().UTC()) 77 | argCount++ 78 | 79 | if len(columns) == 0 { 80 | return 0, errors.New("No fields to update") 81 | } 82 | 83 | args = append(args, book.ID) 84 | 85 | query := fmt.Sprintf(`UPDATE books SET %s WHERE id=$%d RETURNING id`, strings.Join(columns, ", "), argCount) 86 | 87 | var id int 88 | err := s.db.Get(&id, query, args...) 89 | if err != nil { 90 | return 0, err 91 | } 92 | return id, nil 93 | } 94 | 95 | func (s *Storage) DeleteBook(ctx context.Context, id int) error { 96 | _, err := s.db.Exec(`DELETE FROM books WHERE id=$1`, id) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func (s *Storage) VerifyBookExists(ctx context.Context, id int) (bool, error) { 105 | var exists bool 106 | err := s.db.Get(&exists, `SELECT EXISTS(SELECT 1 from books where id=$1)`, id) 107 | if err != nil { 108 | return false, err 109 | } 110 | 111 | return exists, nil 112 | } 113 | -------------------------------------------------------------------------------- /internal/storage/postgres_db.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/golang-migrate/migrate" 8 | "github.com/golang-migrate/migrate/database/postgres" 9 | _ "github.com/golang-migrate/migrate/source/file" // import file driver for migrate 10 | "github.com/jmoiron/sqlx" 11 | "github.com/sash20m/go-api-template/pkg/logger" 12 | ) 13 | 14 | func NewPostgresDB() (*Storage, error) { 15 | 16 | connStr := "host=" + os.Getenv("DB_HOST") + " user=" + os.Getenv("DB_USER") + " dbname=" + os.Getenv("DB_NAME") + " password=" + os.Getenv("DB_PASSWORD") + " sslmode=disable" 17 | logger.Log.Info(connStr, " eee") 18 | 19 | db, err := sqlx.Connect("postgres", connStr) 20 | if err != nil { 21 | return &Storage{}, err 22 | } 23 | 24 | return &Storage{db: db}, nil 25 | } 26 | 27 | // MigratePostgres migrates the postgres db to a new version. 28 | func (s *Storage) MigratePostgres(migrationsPath string) error { 29 | pgStorage, err := postgres.WithInstance(s.db.DB, &postgres.Config{}) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | m, err := migrate.NewWithDatabaseInstance(migrationsPath, "postgres", pgStorage) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | if err := m.Up(); err != nil { 40 | if errors.Is(err, migrate.ErrNoChange) { 41 | logger.Log.Info("No migrations to run") 42 | } else { 43 | return err 44 | } 45 | } 46 | 47 | logger.Log.Info("Migrations script ran successfully") 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jmoiron/sqlx" 7 | _ "github.com/lib/pq" // Initializes the postgres driver 8 | "github.com/sash20m/go-api-template/internal/model" 9 | ) 10 | 11 | type StorageInterface interface { 12 | AddBook(ctx context.Context, book model.AddBookRequest) (int, error) 13 | GetBook(ctx context.Context, id int) (model.Book, error) 14 | GetBooks(ctx context.Context) ([]model.Book, error) 15 | UpdateBook(ctx context.Context, book model.UpdateBookRequest) (int, error) 16 | DeleteBook(ctx context.Context, id int) error 17 | VerifyBookExists(ctx context.Context, id int) (bool, error) 18 | } 19 | 20 | // Storage contains an SQL db. Storage implements the StorageInterface. 21 | type Storage struct { 22 | db *sqlx.DB 23 | } 24 | 25 | func (s *Storage) Close() error { 26 | if err := s.db.Close(); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (s *Storage) GetDB() *sqlx.DB { 34 | return s.db 35 | } 36 | -------------------------------------------------------------------------------- /migrations/1_create_books_table.up.sql: -------------------------------------------------------------------------------- 1 | -- This migration is intended to be for Postgres. Make another one for your db if the SQL is not accepted by your db. 2 | CREATE EXTENSION IF NOT EXISTS citext; 3 | 4 | DO $$ 5 | BEGIN 6 | -- Check if the domain 'email' does not exist 7 | IF NOT EXISTS ( 8 | SELECT 1 9 | FROM pg_type 10 | WHERE typname = 'email' 11 | ) THEN 12 | -- Create the domain if it doesn't exist 13 | CREATE DOMAIN email AS citext CHECK ( 14 | value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' 15 | ); 16 | END IF; 17 | END 18 | $$; 19 | 20 | CREATE TABLE IF NOT EXISTS books ( 21 | /* Use uuid if desired more security, but be aware of performance reduction with big DBs*/ 22 | id serial primary key, 23 | title varchar(255), 24 | author varchar(255), 25 | cover_url varchar(255), 26 | post_url varchar(255), 27 | created_at timestamp, 28 | updated_at timestamp, 29 | CONSTRAINT id_unique UNIQUE (id) 30 | ); 31 | 32 | 33 | -------------------------------------------------------------------------------- /pkg/httputils/http_response.go: -------------------------------------------------------------------------------- 1 | package httputils 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | localErrs "github.com/sash20m/go-api-template/internal/errors" 9 | "github.com/unrolled/render" 10 | ) 11 | 12 | // SuccessfulResponse standardizes responses with 200-299 status code 13 | type SuccessfulResponse struct { 14 | Data interface{} `json:"data"` 15 | Timestamp time.Time `json:"timestamp"` 16 | } 17 | 18 | // ClientErrorResponse standardizes responses with 400-499 status code 19 | type ClientErrorResponse struct { 20 | StatusCode int `json:"statusCode"` 21 | Error localErrs.Err `json:"error"` 22 | Timestamp time.Time `json:"timestamp"` 23 | } 24 | 25 | // ServerErrorResponse standardizes responses with 500-599 status code 26 | type ServerErrorResponse struct { 27 | StatusCode int `json:"statusCode"` 28 | Error localErrs.Err `json:"error"` 29 | Timestamp time.Time `json:"timestamp"` 30 | } 31 | 32 | type Sender struct { 33 | Render *render.Render 34 | } 35 | 36 | // JSON formats the v in a json format and sends it to the client with w. If the statusCode 37 | // is specific for a error, then v is assumed to be a string, an error or an custom errors.Err struct 38 | func (s *Sender) JSON(w http.ResponseWriter, statusCode int, v interface{}) error { 39 | w.Header().Set("Content-Type", "application/json") 40 | codeClass := statusCode / 100 41 | 42 | if codeClass == 2 { 43 | response := SuccessfulResponse{Data: v, Timestamp: time.Now().UTC()} 44 | err := s.Render.JSON(w, statusCode, response) 45 | return err 46 | } 47 | 48 | if codeClass == 4 { 49 | errResponse := localErrs.Err{Message: fmt.Sprint(v)} 50 | 51 | if errInfo, ok := v.(localErrs.Err); ok { 52 | errResponse.Message = errInfo.Error() 53 | errResponse.Data = errInfo.Data 54 | } 55 | 56 | response := ClientErrorResponse{StatusCode: statusCode, Error: errResponse, Timestamp: time.Now().UTC()} 57 | err := s.Render.JSON(w, statusCode, response) 58 | return err 59 | } 60 | 61 | if codeClass == 5 { 62 | errResponse := localErrs.Err{Message: fmt.Sprint(v)} 63 | 64 | if errInfo, ok := v.(localErrs.Err); ok { 65 | errResponse.Message = errInfo.Error() 66 | errResponse.Data = errInfo.Data 67 | } 68 | 69 | response := ServerErrorResponse{StatusCode: statusCode, Error: errResponse, Timestamp: time.Now().UTC()} 70 | err := s.Render.JSON(w, statusCode, response) 71 | return err 72 | } 73 | 74 | err := s.Render.JSON(w, statusCode, v) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // Other formats... (xml, html etc) 83 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // OutputLog is used to output logs to a external .log file that ideally should 10 | // not be in this root directory. Any other configuration to OutputLog can be made here. 11 | var OutputLog = logrus.New() 12 | 13 | func init() { 14 | // The logs directory path has been set to this root directory, but do not keep them here. Add your own path to a 15 | // directory outside of the root project, like /var/log/myapp for Unix/Linux systems, where you can separate the concerns 16 | // or create log rotation if needed. 17 | file, err := os.OpenFile("logs/api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0664) 18 | if err != nil { 19 | logrus.Fatal("Failed to open log file: ", err) 20 | } 21 | OutputLog.Out = file 22 | } 23 | 24 | // Log is used to output the logs to the console in the development mode. 25 | // Any other configuration to Log can be made here. 26 | var Log = logrus.New() 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Golang Rest API Template 2 | > Golang Rest API Template with clear, scalable structure that can sustain large APIs. 3 | 4 | ## Table of Contents 5 | 6 | - [Features](#Features) 7 | - [Directory Structure](#Directory-Structure) 8 | - [Description](#Description) 9 | - [Setup](#Setup) 10 | - [Template Tour](#Template-Tour) 11 | - [License](#license) 12 | 13 | ## Features 14 | 15 | - Standard responses for success and fail requests 16 | - Swagger API documentation 17 | - Sqlx DB with Postgres - but can be changed as needed. 18 | - Standard for custom errors 19 | - Logger for console and external file. 20 | - Migrations setup 21 | - Hot Reload 22 | - Docker setup 23 | - Intuitive, clean and scalabe structure 24 | --- 25 | 26 | ## Directory Structure 27 | ``` 28 | - /cmd --> Contains the app's entry-points 29 | |- /server 30 | |- /docs 31 | |- main.go 32 | |- Makefile 33 | |- /another_binary 34 | - /config --> Contains the config structures that the server uses. 35 | - internal --> Contains the app's code 36 | |- /errors 37 | |- /handlers 38 | |- /middleware 39 | |- /model 40 | |- /storage 41 | |- server.go 42 | - /logs --> The folder's files are not in version control. Here you'll have the logs of the apps (the ones you specify to be external) 43 | - /migrations --> Migrations to set up the database schema in your db. 44 | - /pkg --> Packages used in /internal 45 | |- /httputils 46 | |- /logger 47 | - .air.toml 48 | - .env --> Not in version control. Need to create your own - see below. 49 | - .gitignore 50 | - docker-compose.yml 51 | - Dockerfile 52 | - go.mod 53 | - go.sum 54 | - LICENSE 55 | - README.md 56 | ``` 57 | 58 | ## Description 59 | 60 | **The Why** 61 | 62 | I have spent a while looking for Go Api templates to automate my workflow but I had some trouble with the ones I've found. Some of them were way too specific, sometimes allowing just one set of handlers or one model in db to exist by default, in which case I had to rewrite and restructure it to make it open for extension, to add more handlers, DBs etc. Others were way to complex, with a deep hierarchical structure that didn't make sense (to me at least), half of which I would delete afterwards. So I wanted something that is as flat as possible but still having some structure that is easily extendable when needed, and also has the basic functionality set up so that I only need to add to it. This template is my attempt at achieving that. 63 | 64 | To keep it simple, the template creates a CRUD of books which then can be deleted for your specific handlers, but they show the way the api works generally. The server uses `gorilla/mux` as a router, `urfave/negroni` as a base middleware, `sirupsen/logrus` as a logger and `unrolled/render` as the functionality to format the responses in any way you want before automatically sending them. The API also uses `unrolled/secure` to improve the security. For database management `jmoiron/sqlx` is used to improve the ease of use. 65 | 66 | The app uses Air as a hot-reloader and Docker if you need it. You can start it in both ways (see [Setup](#setup)). 67 | For environment variables `joho/godotenv` has been used instead of a `.yaml` file for security considerations. Again, everything is extendable, so you can add a `.yaml` file if you need more hierarchical structure or your environment variables don't need to be secure. 68 | If you decide to not use Docker, in dev mode you use the variables from .env, and in production you add them in the terminal or in `~/.bash_profile`/`/etc/environment`. If you do decide to use Docker, keep the variables in .env in development mode, and add another .env on your production server with the prod variables. The .env file is not version controlled so there will not be conflicts. I chose to go with this approach from hundreds because I tried to hit the middle - simple and relatively secure, in a way that *most* people will use this template. If the api becomes large and you have specific needs for your case, you can add the variables in prod in command-line, in docker volumes, in a secret manager, in your kubernetes/docker swarm or any other way you want/need. But for most people and most cases, this approach will be more than enough. 69 | 70 | I made sure you can add to it and modify without any pain, so for example, you can add one more db without modifying anything from the existing code, and also you can change the current db (Postgres) with any other also by not modifying anything besides creating the connect function for your new db. Same goes for response senders or for handlers - you can add a new file in /handlers and add your users, auth, products handlers etc. so that the structures remains flat, with maintaining clarity of what is where. 71 | 72 | Also, the responses that the server gives follow a standard, one for 200+ status codes, and another one for the errors: 400+, 500+ status codes. This is implemented by the sender that you'll use to respond to requests. 73 | 74 | ## Setup 75 | 76 | Make sure to first install the binaries that will generate the api docs and hot-reload the app. 77 | 78 | ``` 79 | go install github.com/swaggo/swag/cmd/swag@latest 80 | ``` 81 | and 82 | ``` 83 | go install github.com/cosmtrek/air@latest 84 | ``` 85 | 86 | Download the libs 87 | ``` 88 | go mod download 89 | ``` 90 | ``` 91 | go mod tidy 92 | ``` 93 | 94 | Create an `.env` file in the root folder and use this template: 95 | ``` 96 | # DEV, STAGE, PROD 97 | ENV=DEV 98 | PORT=8080 99 | VERSION=0.0.1 100 | 101 | DB_HOST=localhost #when running the app without docker 102 | # DB_HOST=postgres # when running the app in docker 103 | DB_USER=postgres 104 | DB_NAME=postgres 105 | DB_PASSWORD=postgres 106 | DB_PORT=5432 107 | ``` 108 | 109 | If you start the app locally without docker make sure your Postgres DB is up. 110 | Write `air` in terminal and the app will be up and running listening on whatever port you set in .env. 111 | 112 | Don't forget to rename the module and all the imports in the code from my github link to yours. 113 | 114 | ## Template Tour 115 | Going from top to bottom, in the `/cmd` folder you'll see the entrypoint of the server. In its main.go you'll see the app loading the env and making a config struct which is based on the `/config` also present the root folder. This config will then be passed to the server, it's the only config and you probably don't need another one, just add to it if you want to pass more info to the server from env. The goroutine at the end of the function has the purpose of running a `OnShutdown` function when the server crashes or panics. You can add anything you want in the `OnShutdown` function, I left it empty but with some comments. In the goroutine we make a instance of our server struct which includes our whole server with the db, handlers and everything packed together. Also in `/server` you have a `Makefile` which builds the app properly in the same folder. 116 | 117 | The `/internal` is where the actual code lies. The server creation is in the `server.go` (the `AppServer` struct that includes everything I said earlier). In the `Run` function of this struct we have all the server setup, adding the config info received from `/cmd`, the creation of the db, the router, middlewares and handlers, running the migrations and at the end we start everything up. The `Sender` and `Storage` fields on the struct are injected from `handlers.Handlers` thus the handlers will have access to them. The other 3 functions attached to the struct are `OnShutdown` that I mentioned earlier, `NotFoundHandler` and`NotAllowedHandler` that are 2 special handlers that handle the 404 and 405 status codes. 118 | 119 | In `/errors` you have the standard for custom errors. You use this struct when you desire to respond to a request with some specific error info. This struct will be passed to the `Sender` which we'll see in a bit, and there the `Sender` will structure the response based on a specific standard. Below this there are the sentinels errors that you'll use throughout your app. The `Sender` accepts both a `string` as an error, an normal struct that implements the `error` interface and an `Err` struct that implements the `error` interface but has additional keys in the struct. 120 | 121 | In `/handlers` you have the `handlers.go` file which creates the handlers object and all the dependencies it needs, which then itself is injected into `AppServer` in `server.go` that I mentioned above. Now, the others `.go` files in this folder should be specific to each handler group you have. By default there's one for books in `books.go`, but if you need handlers for users, you create a `users.go`, for auth an `auth.go` and so on. All the functions there are received by the `Handlers` struct in `handlers.go`. 122 | 123 | `/middlewares` implements custom middlewares that are added into `negroni` at the start of the server in `server.go`. 124 | 125 | In `/model` you first have the `base_model.go` which is the base for the other models. And in the same `/handlers` spirit, each file is responsible for its part of the app, so `books.go` will have the database model for books, the one that reflects the books table in db, and below it you have different structs that define how responses and requests for each endpoint should look like (since you don't always want to send the whole model struct as a response and also sending it with zero values is no good). 126 | 127 | In `/storage` there's the `storage.go` which defines how the storage should look like, thus being able to switch databases. The new database should implement the `StorageInterface` and it's good to go. `postgres_db.go` create a instance of this storage with a postgres db in the `Storage` struct as seen in `server.go` when we created the db. For a new db just create another `my_db.go` and create a `Storage` instance with that particular db. In the `Storage` struct you can add a `redis` db and so on if you want more dbs. Again in the same spirit, `books.go` implements the `StorageInterface` specifically for books, and all the methods are received by the `Storage` struct that is eventually used in handlers. For users you'll have `users.go` and there will be all the db operations specifically on users and so on. 128 | 129 | In `\pkg` you have the `httputils` and there's the `http_response.go` which creates the standard for responses. The `Sender` struct in injected in the handlers and will be used there whenever you send a response. I made this struct with the idea of automation and also with the idea of extension - by default there's the `JSON` response, but you can add below HTML format, XML or any other you need. `s.Render` has a bunch of them. The `JSON` function takes the `http.ResponseWrites`, the statusCode and the struct you want to send back to the user, and based on the format defined above in the file, sends the response. 130 | The `/loger` in pkg defines the logger for the app, you can use this logger to log to console or to the `/logs` file in the root folder, you can see in handlers how it is used. The logs directory path has been set to this root directory, but do not keep them here. Add your own path to a directory outside of the root project, like `/var/log/myapp` for Unix/Linux systems, where you can separate the concerns or create log rotation if needed. 131 | 132 | Thus you have all the minimum functionality you always need to get started on an Api that can grow large with time. 133 | 134 | The other remaining files are self-explanatory. 135 | 136 | Happy coding. 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 141 | 142 | --------------------------------------------------------------------------------