├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── cmd └── web-server │ └── golang-web-server.go ├── configs └── server-config.go ├── go.mod ├── pkg └── mhttp │ ├── functions.go │ └── server.go ├── scripts └── build.sh ├── tests └── golang-mini-web-server.postman_collection.json └── web └── static ├── hello.html ├── homepage.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Binary built by Go for this code 18 | bin -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "args": ["handler_functions"] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Juan Luis Hidalgo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mini Web Server in Go 2 | 3 | A very simple implementation of a **Web Server** in **Go** programming language. 4 | 5 | ## Description 6 | 7 | This project implements a very basic Web Server using nothing else than the [Go Standard Library](https://pkg.go.dev/std). This was created for educational purposes in order to explore the basic functions that are provided by the standard library for *networking*, specifically the [net/http](https://pkg.go.dev/net/http@go1.19.5#pkg-overview) package. The server is capable of performing these operations: 8 | * listen to a specific port for incoming HTTP requests, and 9 | * process the requests and then reply with the corresponding HTTP responses. 10 | 11 | The application can serve the requests by two different ways: 12 | * handler functions: the application registers individual functions to process the requests depending on specific patterns. 13 | * FileServer handler: Serves HTTP requests with the contents of the file system rooted at root. 14 | 15 | This repository also includes a [Postman collection](/tests/golang-mini-web-server.postman_collection.json) that can be used for testing the functionality of the Web server. 16 | 17 | ## Getting started 18 | 19 | ### Dependencies 20 | 21 | This is required for building and compiling the application: 22 | * Go version 1.18.x or higher 23 | 24 | Optionally you will need Postman if you want to run the tests provided [here](/tests/golang-mini-web-server.postman_collection.json). 25 | * Postman 26 | 27 | ### Installing 28 | 29 | 1. Clone this repository 30 | 2. Make the repository folder your working directory 31 | 3. Initialize the module for the application by running these commands from the terminal (give a proper name to your module) 32 | ```bash 33 | go mod init module_name 34 | go mod vendor 35 | ``` 36 | 4. Update the package reference in the *import* section inside the `cmd/web-server/golang-web-server.go` file with the *module_name* that you used in the previous step: 37 | ```golang 38 | "module_name/pkg/handler" 39 | ``` 40 | 5. Compile and build the application to ensure everything works fine. 41 | 42 | In Unix-like systems just execute this script: 43 | ```bash 44 | ./scripts/build.sh 45 | ``` 46 | 47 | Or you can just execute this command as well: 48 | ```bash 49 | go build -o ./bin/web-server cmd/web-server/golang-web-server.go 50 | ``` 51 | This should generate a binary file named `web-server` in the `bin` directory. 52 | 53 | ### Executing the program 54 | 55 | * The simplest way is to run the `web-server` binary file directly: 56 | ```bash 57 | ./bin/web-server 58 | ``` 59 | Expected outcome is: 60 | ```bash 61 | Initializing the handler functions... 62 | Handler functions have been initialized 63 | Listening on localhost:8081... 64 | ``` 65 | **Note**: By default, the application will be executed using handler functions and will listen to port 8081. 66 | 67 | * Alternatively you can also run the application with FileServer handler: 68 | ```bash 69 | ./bin/web-server -use-handler-functions=false 70 | ``` 71 | Expected outcome is: 72 | ```bash 73 | Initializing handler with FileServer 74 | Handler has been initialized 75 | Listening on localhost:8081... 76 | ``` 77 | 78 | ### Testing 79 | 80 | There are two approaches for interacting and testing the application, this can be done through a Web browser or Postman. 81 | 82 | #### Web browser 83 | 84 | Open a Web browser and go to http://localhost:8081. The `index` page should be loaded if everything is working as expected. Examine the source code for the requests that can be processed by the Web server, these vary depending on how the application is serving the requests (handler functions or FileServer handler). 85 | 86 | #### Postman 87 | 88 | Import the [collection](./tests/golang-mini-web-server.postman_collection.json) into Postman and then open the `Golang Mini Web Server` collection. The requests of this collection are grouped into two different folders, one is for handler functions implementation and the other one for FileServer handler implementation. You can run the requests within the folder that matches the current mode of the Web server. 89 | 90 | ## License 91 | This project is licensed under the MIT License - see the LICENSE file for details 92 | 93 | ## Acknowledgments 94 | Inspiration, code snippets, etc. 95 | 96 | * [Creating a simple Web server with Golang](https://tutorialedge.net/golang/creating-simple-web-server-with-golang/) 97 | * [A simple README.md template](https://gist.github.com/DomPizzie/7a5ff55ffa9081f2de27c315f5018afc) 98 | * [Standard Go project layout](https://github.com/golang-standards/project-layout) -------------------------------------------------------------------------------- /cmd/web-server/golang-web-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | "github.com/jlhidalgo/golang-web-server/configs" 8 | "github.com/jlhidalgo/golang-web-server/pkg/mhttp" 9 | ) 10 | 11 | var useHandlerFunctions bool 12 | 13 | func main() { 14 | flag.BoolVar(&useHandlerFunctions, "use-handler-functions", true, "Runs the Web Browser with handler functions, use false to run it in FileServer mode.") 15 | flag.Parse() 16 | 17 | server := mhttp.NewServer(configs.SERVER_STATIC_DIRECTORY, configs.SERVER_URL, configs.SERVER_PORT) 18 | var err error 19 | 20 | if useHandlerFunctions { 21 | err = server.InitializeHandlerFunctions() 22 | } else { 23 | err = server.InitializeFileServer() 24 | } 25 | 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | server.ListenAndServe() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /configs/server-config.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | const ( 4 | SERVER_STATIC_DIRECTORY string = "./web/static" 5 | SERVER_URL string = "localhost" 6 | SERVER_PORT string = "8081" 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jlhidalgo/golang-web-server 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /pkg/mhttp/functions.go: -------------------------------------------------------------------------------- 1 | package mhttp 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | var counter int 11 | var mutex = &sync.Mutex{} 12 | var staticDirectory string 13 | 14 | // Replies to the request with the contents of the named file or directory 15 | func serveFile(w http.ResponseWriter, r *http.Request) { 16 | http.ServeFile(w, r, staticDirectory) 17 | } 18 | 19 | // Increments a counter every time the '/increment' page is requested 20 | // additionally the counter is replied back to the web browser 21 | func incrementCounter(w http.ResponseWriter, r *http.Request) { 22 | mutex.Lock() 23 | counter++ 24 | fmt.Fprint(w, strconv.Itoa(counter)) 25 | mutex.Unlock() 26 | } 27 | 28 | // Just replies with the word 'Hi' as the body of the page 29 | func hiString(w http.ResponseWriter, r *http.Request) { 30 | fmt.Fprintf(w, "Hi") 31 | } 32 | -------------------------------------------------------------------------------- /pkg/mhttp/server.go: -------------------------------------------------------------------------------- 1 | package mhttp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | type IServer interface { 12 | InitializeFileServer() error 13 | InitializeHandlerFunctions() error 14 | ListenAndServe() 15 | } 16 | 17 | type ServerConfig struct { 18 | staticDirectory string 19 | url string 20 | port string 21 | } 22 | 23 | func NewServer(staticDir string, url string, port string) ServerConfig { 24 | var serverConfig = ServerConfig{ 25 | staticDirectory: staticDir, 26 | url: url, 27 | port: port, 28 | } 29 | return serverConfig 30 | } 31 | 32 | // returns a handler that serves HTTP requests with the contents of the file system rooted at root 33 | func (fs ServerConfig) InitializeFileServer() error { 34 | fmt.Println("Initializing handler with FileServer") 35 | 36 | if err := validateFolder(fs.staticDirectory); err != nil { 37 | return errors.New("couldn't validate static folder, error: " + err.Error()) 38 | } 39 | 40 | http.Handle("/", http.FileServer(http.Dir(fs.staticDirectory))) 41 | fmt.Println("Handler has been initialized") 42 | return nil 43 | } 44 | 45 | // starts listenning the port and serves the requests 46 | func (fs ServerConfig) ListenAndServe() { 47 | serverUrl := fs.url + ":" + fs.port 48 | fmt.Printf("Listening on %s...\n", serverUrl) 49 | log.Fatal(http.ListenAndServe(serverUrl, nil)) 50 | } 51 | 52 | // Initializes all the handler functions 53 | func (fs ServerConfig) InitializeHandlerFunctions() error { 54 | staticDirectory = fs.staticDirectory 55 | fmt.Println("Initializing the handler functions...") 56 | 57 | if err := validateFolder(fs.staticDirectory); err != nil { 58 | return errors.New("couldn't validate static folder, error: " + err.Error()) 59 | } 60 | 61 | http.HandleFunc("/", serveFile) 62 | http.HandleFunc("/increment", incrementCounter) 63 | http.HandleFunc("/hi", hiString) 64 | fmt.Println("Handler functions have been initialized") 65 | return nil 66 | } 67 | 68 | func validateFolder(folderPath string) error { 69 | fileInfo, err := os.Stat(folderPath) 70 | 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if fileInfo != nil && !fileInfo.IsDir() { 76 | return fmt.Errorf("%s : is not a directory", folderPath) 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go build -o ./bin/web-server ./cmd/web-server/golang-web-server.go -------------------------------------------------------------------------------- /tests/golang-mini-web-server.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "9a24d3a1-6241-424e-8843-8731058e95cd", 4 | "name": "Golang Mini Web Server", 5 | "description": "# Overview\n\nThis collection contains the allowed requests for the Golang Web Server project which is hosted at: [https://github.com/jlhidalgo/golang-web-server](https://github.com/jlhidalgo/golang-web-server)\n\nThese requests will be typically used for:\n1. Running verification over the Golang Web Server in order to confirm that the application is working as expected, and\n2. Providing a guidance on how the Web Server works and what is capable of doing", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 7 | "_exporter_id": "6679410" 8 | }, 9 | "item": [ 10 | { 11 | "name": "Handler Functions", 12 | "item": [ 13 | { 14 | "name": "Get root", 15 | "event": [ 16 | { 17 | "listen": "test", 18 | "script": { 19 | "exec": [ 20 | "pm.test(\"Status code is 200\", function () {", 21 | " pm.response.to.have.status(200);", 22 | "});", 23 | "", 24 | "pm.test(\"Index page is obtained\", function () {", 25 | " pm.expect(pm.response.text()).to.include(\"Index\");", 26 | "});" 27 | ], 28 | "type": "text/javascript" 29 | } 30 | } 31 | ], 32 | "request": { 33 | "method": "GET", 34 | "header": [], 35 | "url": { 36 | "raw": "http://localhost:8081/", 37 | "protocol": "http", 38 | "host": [ 39 | "localhost" 40 | ], 41 | "port": "8081", 42 | "path": [ 43 | "" 44 | ] 45 | } 46 | }, 47 | "response": [] 48 | }, 49 | { 50 | "name": "Get hi", 51 | "event": [ 52 | { 53 | "listen": "test", 54 | "script": { 55 | "exec": [ 56 | "pm.test(\"Status code is 200\", function () {", 57 | " pm.response.to.have.status(200);", 58 | "});", 59 | "", 60 | "pm.test(\"Hi page is obtained\", function () {", 61 | " pm.expect(pm.response.text()).to.include(\"Hi\");", 62 | "});" 63 | ], 64 | "type": "text/javascript" 65 | } 66 | } 67 | ], 68 | "request": { 69 | "method": "GET", 70 | "header": [], 71 | "url": { 72 | "raw": "http://localhost:8081/hi", 73 | "protocol": "http", 74 | "host": [ 75 | "localhost" 76 | ], 77 | "port": "8081", 78 | "path": [ 79 | "hi" 80 | ] 81 | } 82 | }, 83 | "response": [] 84 | }, 85 | { 86 | "name": "Get counter incremented", 87 | "event": [ 88 | { 89 | "listen": "test", 90 | "script": { 91 | "exec": [ 92 | "var localCounter = pm.collectionVariables.get(\"counter\");", 93 | "", 94 | "pm.test(\"Status code is 200\", function () {", 95 | " pm.response.to.have.status(200);", 96 | "});", 97 | "", 98 | "pm.test(\"Counter is provided in body\", function () {", 99 | " var isNumber = !isNaN(pm.response.text());", 100 | " pm.expect(isNumber).to.be.true;", 101 | "});", 102 | "", 103 | "pm.test(\"Counter has been incremented\", function () {", 104 | " var newCounter = Number.parseInt(pm.response.text());", 105 | " pm.expect(newCounter).to.be.greaterThan(Number.parseInt(localCounter));", 106 | " pm.collectionVariables.set(\"counter\", newCounter);", 107 | "});", 108 | "" 109 | ], 110 | "type": "text/javascript" 111 | } 112 | } 113 | ], 114 | "request": { 115 | "method": "GET", 116 | "header": [], 117 | "url": { 118 | "raw": "http://localhost:8081/increment", 119 | "protocol": "http", 120 | "host": [ 121 | "localhost" 122 | ], 123 | "port": "8081", 124 | "path": [ 125 | "increment" 126 | ] 127 | } 128 | }, 129 | "response": [] 130 | } 131 | ], 132 | "description": "# Description\n\nThis folder contains the requests that are served when the Golang Web Server is launched with Handler Functions. See more about this in the Readme file of the project: [https://github.com/jlhidalgo/golang-web-server/blob/main/README.md](https://github.com/jlhidalgo/golang-web-server/blob/main/README.md)" 133 | }, 134 | { 135 | "name": "FileServer Handler", 136 | "item": [ 137 | { 138 | "name": "Get root", 139 | "event": [ 140 | { 141 | "listen": "test", 142 | "script": { 143 | "exec": [ 144 | "pm.test(\"Status code is 200\", function () {", 145 | " pm.response.to.have.status(200);", 146 | "});", 147 | "", 148 | "pm.test(\"Index page is obtained\", function () {", 149 | " pm.expect(pm.response.text()).to.include(\"Index\");", 150 | "});" 151 | ], 152 | "type": "text/javascript" 153 | } 154 | } 155 | ], 156 | "request": { 157 | "method": "GET", 158 | "header": [], 159 | "url": { 160 | "raw": "http://localhost:8081/", 161 | "protocol": "http", 162 | "host": [ 163 | "localhost" 164 | ], 165 | "port": "8081", 166 | "path": [ 167 | "" 168 | ] 169 | } 170 | }, 171 | "response": [] 172 | }, 173 | { 174 | "name": "Get index", 175 | "event": [ 176 | { 177 | "listen": "test", 178 | "script": { 179 | "exec": [ 180 | "pm.test(\"Status code is 200\", function () {", 181 | " pm.response.to.have.status(200);", 182 | "});", 183 | "", 184 | "pm.test(\"Index page is obtained\", function () {", 185 | " pm.expect(pm.response.text()).to.include(\"Index\");", 186 | "});" 187 | ], 188 | "type": "text/javascript" 189 | } 190 | } 191 | ], 192 | "request": { 193 | "method": "GET", 194 | "header": [], 195 | "url": { 196 | "raw": "http://localhost:8081/index.html", 197 | "protocol": "http", 198 | "host": [ 199 | "localhost" 200 | ], 201 | "port": "8081", 202 | "path": [ 203 | "index.html" 204 | ] 205 | } 206 | }, 207 | "response": [] 208 | }, 209 | { 210 | "name": "Get index - extension not provided", 211 | "event": [ 212 | { 213 | "listen": "test", 214 | "script": { 215 | "exec": [ 216 | "pm.test(\"Status code is 404\", function () {", 217 | " pm.response.to.have.status(404);", 218 | "});", 219 | "" 220 | ], 221 | "type": "text/javascript" 222 | } 223 | } 224 | ], 225 | "request": { 226 | "method": "GET", 227 | "header": [], 228 | "url": { 229 | "raw": "http://localhost:8081/index", 230 | "protocol": "http", 231 | "host": [ 232 | "localhost" 233 | ], 234 | "port": "8081", 235 | "path": [ 236 | "index" 237 | ] 238 | } 239 | }, 240 | "response": [] 241 | }, 242 | { 243 | "name": "Get home page", 244 | "event": [ 245 | { 246 | "listen": "test", 247 | "script": { 248 | "exec": [ 249 | "pm.test(\"Status code is 200\", function () {", 250 | " pm.response.to.have.status(200);", 251 | "});", 252 | "", 253 | "pm.test(\"Home page is obtained\", function () {", 254 | " pm.expect(pm.response.text()).to.include(\"home page\");", 255 | "});" 256 | ], 257 | "type": "text/javascript" 258 | } 259 | } 260 | ], 261 | "request": { 262 | "method": "GET", 263 | "header": [], 264 | "url": { 265 | "raw": "http://localhost:8081/homepage.html", 266 | "protocol": "http", 267 | "host": [ 268 | "localhost" 269 | ], 270 | "port": "8081", 271 | "path": [ 272 | "homepage.html" 273 | ] 274 | } 275 | }, 276 | "response": [] 277 | }, 278 | { 279 | "name": "Get hi - page not served", 280 | "event": [ 281 | { 282 | "listen": "test", 283 | "script": { 284 | "exec": [ 285 | "pm.test(\"Status code is 404\", function () {", 286 | " pm.response.to.have.status(404);", 287 | "});", 288 | "" 289 | ], 290 | "type": "text/javascript" 291 | } 292 | } 293 | ], 294 | "request": { 295 | "method": "GET", 296 | "header": [], 297 | "url": { 298 | "raw": "http://localhost:8081/hi", 299 | "protocol": "http", 300 | "host": [ 301 | "localhost" 302 | ], 303 | "port": "8081", 304 | "path": [ 305 | "hi" 306 | ] 307 | } 308 | }, 309 | "response": [] 310 | } 311 | ], 312 | "description": "# Description\n\nThis folder contains the requests that are served when the Golang Web Server is launched with the FileServer handler option. See more about this in the Readme file of the project: [https://github.com/jlhidalgo/golang-web-server/blob/main/README.md](https://github.com/jlhidalgo/golang-web-server/blob/main/README.md)" 313 | } 314 | ], 315 | "event": [ 316 | { 317 | "listen": "prerequest", 318 | "script": { 319 | "type": "text/javascript", 320 | "exec": [ 321 | "" 322 | ] 323 | } 324 | }, 325 | { 326 | "listen": "test", 327 | "script": { 328 | "type": "text/javascript", 329 | "exec": [ 330 | "" 331 | ] 332 | } 333 | } 334 | ], 335 | "variable": [ 336 | { 337 | "key": "counter", 338 | "value": "0", 339 | "type": "string" 340 | } 341 | ] 342 | } -------------------------------------------------------------------------------- /web/static/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World 4 | 5 | 6 |

Hello World!

7 | 8 | 9 | -------------------------------------------------------------------------------- /web/static/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Home Page 4 | 5 | 6 |

Welcome to the home page!

7 | 8 | 9 | -------------------------------------------------------------------------------- /web/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Index 4 | 5 | 6 |

Index

7 | 8 | 9 | --------------------------------------------------------------------------------