├── .gitignore ├── LICENSE ├── Readme.md ├── api-with-echo ├── Readme.md ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── go.mod ├── go.sum └── main.go ├── api-with-gorilla-mux ├── Readme.md ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── go.mod ├── go.sum └── main.go ├── api-with-net-http ├── Readme.md ├── go.mod └── main.go ├── middleware-security-echo ├── Readme.md ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── go.mod ├── go.sum └── main.go ├── middleware-security ├── Readme.md ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── go.mod ├── go.sum └── main.go ├── project-structure ├── Readme.md ├── flat-structure │ ├── Readme.md │ ├── data │ │ ├── courses.json │ │ ├── instructors.json │ │ └── users.json │ ├── go.mod │ ├── go.sum │ ├── handlers.go │ ├── main.go │ ├── middlewares.go │ └── types.go └── pkg-structure │ ├── Readme.md │ ├── cmd │ └── web │ │ └── main.go │ ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json │ ├── go.mod │ ├── go.sum │ ├── internal │ ├── handler │ │ ├── authenticated.go │ │ ├── course.go │ │ ├── instructor.go │ │ ├── server.go │ │ ├── types.go │ │ ├── user.go │ │ └── utils.go │ └── middleware │ │ ├── jwt.go │ │ └── types.go │ └── pkg │ └── middleware │ └── logger.go ├── rest-api-container ├── .env.example ├── .gitignore ├── Dockerfile ├── Readme.md ├── cmd │ └── web │ │ └── main.go ├── database │ └── migration │ │ ├── 000001_init.down.sql │ │ └── 000001_init.up.sql ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal │ ├── datasource │ │ ├── db.go │ │ ├── postgres.go │ │ ├── postgres_test.go │ │ ├── sqldb.go │ │ └── type.go │ └── handler │ │ ├── authenticated.go │ │ ├── courses.go │ │ ├── courses_test.go │ │ ├── handler.go │ │ ├── instructors.go │ │ ├── type.go │ │ ├── users.go │ │ └── util.go ├── kubernetes │ ├── deployment.yaml │ ├── secret.example.yaml │ ├── service-account.yaml │ └── service.yaml └── pkg │ ├── database │ └── postgres.go │ └── middleware │ ├── jwt.go │ └── logger.go ├── rest-api-database ├── .env.example ├── Readme.md ├── cmd │ └── web │ │ └── main.go ├── database │ └── migration │ │ ├── 000001_init.down.sql │ │ └── 000001_init.up.sql ├── go.mod ├── go.sum ├── internal │ ├── datasource │ │ ├── db.go │ │ ├── postgres.go │ │ ├── postgres_test.go │ │ └── type.go │ └── handler │ │ ├── authenticated.go │ │ ├── courses.go │ │ ├── courses_test.go │ │ ├── handler.go │ │ ├── instructors.go │ │ ├── type.go │ │ ├── users.go │ │ └── util.go └── pkg │ ├── database │ └── postgres.go │ └── middleware │ ├── jwt.go │ └── logger.go ├── restapi-docs-echo ├── Readme.md ├── cmd │ └── web │ │ └── main.go ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum ├── internal │ └── handler │ │ ├── authenticated.go │ │ ├── courses.go │ │ ├── courses_test.go │ │ ├── instructors.go │ │ ├── type.go │ │ ├── users.go │ │ └── util.go └── pkg │ └── middleware │ ├── jwt.go │ └── logger.go ├── testing-benchmark-echo ├── Readme.md ├── cmd │ └── web │ │ └── main.go ├── data │ ├── courses.json │ ├── instructors.json │ └── users.json ├── go.mod ├── go.sum ├── internal │ └── handler │ │ ├── authenticated.go │ │ ├── courses.go │ │ ├── courses_test.go │ │ ├── instructors.go │ │ ├── type.go │ │ ├── users.go │ │ └── util.go └── pkg │ └── middleware │ ├── jwt.go │ └── logger.go └── testing-benchmark ├── Readme.md ├── cmd └── web │ └── main.go ├── data ├── courses.json ├── instructors.json └── users.json ├── go.mod ├── go.sum ├── internal ├── handler │ ├── authenticated.go │ ├── course.go │ ├── course_test.go │ ├── instructor.go │ ├── server.go │ ├── types.go │ ├── user.go │ └── utils.go └── middleware │ ├── jwt.go │ └── types.go └── pkg └── middleware └── logger.go /.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 | .env 17 | .idea/ 18 | 19 | .DS_Store -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # RESTful Go API 2 | 3 | This repo holds the code content for the RESTful GO API live trainging session. 4 | 5 | This content will continue to grow as I learn more and get your feedback. 6 | 7 | If any section is not 100% clear open an issue. If you see anything that you can fix, create a PR. 8 | 9 | ## Structure 10 | 11 | In order to gradually build up the conent for optimal learning, I have decided to make use of git branches. Each branch 12 | name will have the format `-01..n`. 13 | 14 | Each topic will be in their own folder and will be a complete go project. 15 | 16 | At each level of the workshop the branch should be working code. If it is not I will mention it. 17 | 18 | ## Sections 19 | 20 | 1. Standard Library net/http 21 | - [Getting Started](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-01/api-with-net-http#run-the-example) 22 | - [Custom Handler Type](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-02/api-with-net-http#why-a-struct) 23 | - [JSON Response](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-03/api-with-net-http#json) 24 | - [HTTP Verbs](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-04/api-with-net-http#http-verbs) 25 | - [Request Body](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-05/api-with-net-http#rest-routes) 26 | - [Handler vs HandlerFunc vs *HandlerMethod](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-06/api-with-net-http#handler-vs-handlerfunc-vs-handlermethod) 27 | - [Path Parameter](https://github.com/moficodes/restful-go-api/tree/standard-library-net-http-07/api-with-net-http#path-parameter) 28 | 29 | 2. a. Gorilla Mux 30 | - [Why Gorilla Mux](https://github.com/moficodes/restful-go-api/tree/gorilla-mux-01/api-with-gorilla-mux#why-gorilla-mux) 31 | - [Path Parameter](https://github.com/moficodes/restful-go-api/tree/gorilla-mux-02/api-with-gorilla-mux#path-params) 32 | - [Query Parameter](https://github.com/moficodes/restful-go-api/tree/gorilla-mux-03/api-with-gorilla-mux#query-parameters) 33 | - [Match Query](https://github.com/moficodes/restful-go-api/tree/gorilla-mux-04/api-with-gorilla-mux#match-query) 34 | - [Sub Router](https://github.com/moficodes/restful-go-api/tree/gorilla-mux-05/api-with-gorilla-mux#sub-router) 35 | 36 | b. Echo 37 | - [Why Echo](https://github.com/moficodes/restful-go-api/tree/echo-01/api-with-echo#why-echo) 38 | - [Binding Parameters](https://github.com/moficodes/restful-go-api/tree/echo-02/api-with-echo#binding-parameters) 39 | - [Sub Router / Groups](https://github.com/moficodes/restful-go-api/tree/echo-03/api-with-echo#group) 40 | 41 | 3. a. Middleware and Security with Gorilla Mux 42 | - [Middleware](https://github.com/moficodes/restful-go-api/tree/middleware-security-01/middleware-security#middleware) 43 | - [Chaining Middlewares](https://github.com/moficodes/restful-go-api/tree/middleware-security-02/middleware-security#chaining-middlewares) 44 | - [Mux Handlers](https://github.com/moficodes/restful-go-api/tree/middleware-security-03/middleware-security#mux-handlers) 45 | - [JWT Auth](https://github.com/moficodes/restful-go-api/tree/middleware-security-04/middleware-security#jwt-authentication) 46 | 47 | b. Middleware and Security with Echo 48 | - [Middleware](https://github.com/moficodes/restful-go-api/tree/middleware-echo-01/middleware-security-echo#middleware) 49 | - [Chaining & Echo Middlewares](https://github.com/moficodes/restful-go-api/tree/middleware-echo-02/middleware-security-echo#chaining-middleware) 50 | - [JWT Auth](https://github.com/moficodes/restful-go-api/tree/middleware-echo-03/middleware-security-echo#jwt) 51 | - [JWT Auth with EchoJWT](https://github.com/moficodes/restful-go-api/tree/middleware-echo-04/middleware-security-echo#jwt) 52 | 4. Project Structure 53 | - [Common Project Structures for Go application](https://github.com/moficodes/restful-go-api/tree/project-structure-01/project-structure) 54 | 55 | 5. Testing and Benchmarking 56 | - [Unit Testing](https://github.com/moficodes/restful-go-api/tree/testing-benchmarking-01/testing-benchmark) 57 | - [Unit Testing With Echo](https://github.com/moficodes/restful-go-api/tree/testing-benchmarking-echo-01/testing-benchmark-echo) 58 | 59 | 6. Database 60 | - [Postgres Database](https://github.com/moficodes/restful-go-api/tree/rest-api-database-01/rest-api-database#go--postgres) 61 | - [Testing Database](https://github.com/moficodes/restful-go-api/tree/rest-api-database-02/rest-api-database#testing) 62 | 63 | 7. Application Delivery 64 | - [Docker Compose](https://github.com/moficodes/restful-go-api/tree/containers-01/rest-api-container) 65 | - [Kubernetes](https://github.com/moficodes/restful-go-api/tree/containers-02/rest-api-container) 66 | 67 | 8. Docs Generation 68 | - [Swagger](https://github.com/moficodes/restful-go-api/tree/rest-api-docs-01/restapi-docs-echo) -------------------------------------------------------------------------------- /api-with-echo/Readme.md: -------------------------------------------------------------------------------- 1 | # Echo Web Framwork 2 | 3 | In the previous section we saw how we can create a simele REST endpoint with net/http. We saw there were some limitations. But In most cases when we don't need complex path matching, net/http works just fine. 4 | 5 | Lets see how Echo solves those problem. 6 | 7 | ## Run the example 8 | 9 | ```bash 10 | git checkout origin/echo-03 11 | ``` 12 | 13 | If you are not already in the folder 14 | 15 | ```bash 16 | cd api-with-echo 17 | ``` 18 | 19 | ```bash 20 | go run main.go 21 | ``` 22 | 23 | ```bash 24 | curl localhost:7999/api/v1/users 25 | ``` 26 | 27 | ## Why Echo 28 | 29 | In benchmarks echo is one of the faster ones out there. 30 | 31 | The following is from a benchmark result with one parameter. 32 | 33 | | lib/framework | Operations K | ns/op | B/op | allocs/op | 34 | |:-------------:|:------------:|:-----:|:----:|:---------:| 35 | | Beego | 442 | 2791 | 352 | 3 | 36 | | Chi | 1000 | 1006 | 432 | 3 | 37 | | Echo | 14662 | 81.9 | 0 | 0 | 38 | | Gin | 16683 | 72.3 | 0 | 0 | 39 | | Gorilla Mux | 434 | 2943 | 1280 | 10 | 40 | | HttpRouter | 23988 | 50 | 0 | 0 | 41 | 42 | HttpRouter is around 2x faster. So why are we starting with Echo? 43 | 44 | Echo is more full featured compared to some of the routers in the list. It has many built in features that makes building rest api a breeze. 45 | 46 | ## Routing based on Http Method 47 | 48 | With net/http we were sending all Http Verb request to the route and handling each type in the same function. With echo we can specify the http method for each our route. 49 | 50 | ```go 51 | e.GET("/users", getAllUsers) 52 | e.GET("/instructors", getAllInstructors) 53 | e.GET("/courses", getAllCourses) 54 | ``` 55 | 56 | We can curl for response 57 | 58 | ```bash 59 | curl localhost:7999/users 60 | ``` 61 | 62 | ## Binding Parameters 63 | 64 | Echo lets us bind parametes from different sources (path param, query param, request body). We can still reach into the request inside `c` if we want to. But bind is a nice helper that can do type assertions and catch validation errors automatically. 65 | 66 | ```go 67 | if err := echo.QueryParamsBinder(c). 68 | Strings("topics", &topics). 69 | Int("instructor", &instructor). 70 | Strings("attendee", &attendees).BindError(); err != nil { 71 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 72 | } 73 | ``` 74 | 75 | We can do the same thing for path Parameters too. 76 | 77 | ```bash 78 | curl localhost:7999/instructors/1 79 | ``` 80 | 81 | We could also use query parameters like 82 | 83 | ```bash 84 | curl "localhost:7999/api/v1/courses?topic=go&topic=devops" 85 | ``` 86 | 87 | Query parameters don't have any generic syntax that talks about how query params should be formatted. For example you can find multiple different ways to pass these value 88 | 89 | [RFC 3986: Uniform Resource Identifier (URI): Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986) has not no fix guidance on how to do it. You will probably find all these different ways to pass multiple query values for the same key. Its left up to practioners on how to deal with these. 90 | 91 | 1. example.com/path?name=name1&name=name2 92 | 2. example.com/path?name=name1,name2 93 | 3. example.com/path?name=[name1,name2] 94 | 95 | In our code we implement the first way. Its no more right than any other way. Also its upto us to determine whether we are choosing to take in an array or taking only one of the query param. 96 | 97 | For example 98 | 99 | ```bash 100 | curl "localhost:7999/api/v1/courses?instructor=1&instructor=2" 101 | ``` 102 | 103 | will only take the first query to filter by. 104 | 105 | But 106 | 107 | ```bash 108 | curl "localhost:7999/api/v1/courses?topic=go&topic=devops" 109 | ``` 110 | 111 | Will take both query and only return courses that has both `go` and `devops` in their topic. 112 | 113 | So we are implementing is as a logical `and` operator. 114 | 115 | ```bash 116 | curl "localhost:7999/api/v1/users?interest=go&interest=swift&interest=hadoop" 117 | ``` 118 | 119 | will return users who has interest in go, swift and haddop. 120 | 121 | It is also reasonable to implement that as a logical `or` depending on your application. We might want to find any course where userId 1, 2 or 3 signed up. The code currently does not do this. You can take it as an excercise to implement this. 122 | 123 | > Hint: You can look at the `Contains` function to see what you need to change to make it work as logical or. 124 | 125 | As long as we are implementing the behaviour we expect from our application we are fine. 126 | 127 | ## Group 128 | 129 | So far we have been adding all our routes at the top level of our hostname. i.e. `localhost:7999/`. But there are times when it is desired to have routes that are grouped together based on some criteria. For example we might want all our authentication routes grouped together under `/auth` or we could want to version our api with `/api/v1`. With a sub-router it is possible to apply rules and logic to a group of routes instead of applying these rules individually. 130 | 131 | To create a group 132 | 133 | ```go 134 | api := e.Group("api/v1") 135 | ``` 136 | 137 | We can then treat `api` as if it were a instance of echo and add new routes to it. Any route added to this subrouter will be prefixed with `/api/v1` so the path `/users` become `/api/v1/users`. 138 | 139 | We can still test that all our routes work as expected. 140 | 141 | ```bash 142 | curl localhost:7999/api/v1/instructors 143 | ``` 144 | -------------------------------------------------------------------------------- /api-with-echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/echo 2 | 3 | go 1.20 4 | 5 | require github.com/labstack/echo/v4 v4.10.0 6 | 7 | require ( 8 | github.com/labstack/gommon v0.4.0 // indirect 9 | github.com/mattn/go-colorable v0.1.13 // indirect 10 | github.com/mattn/go-isatty v0.0.17 // indirect 11 | github.com/valyala/bytebufferpool v1.0.0 // indirect 12 | github.com/valyala/fasttemplate v1.2.2 // indirect 13 | golang.org/x/crypto v0.6.0 // indirect 14 | golang.org/x/net v0.6.0 // indirect 15 | golang.org/x/sys v0.5.0 // indirect 16 | golang.org/x/text v0.7.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /api-with-echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= 5 | github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= 6 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 7 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 8 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 9 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 10 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 11 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 12 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 13 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 14 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 20 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 21 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 22 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 23 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 24 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 25 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 26 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 27 | golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= 28 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 29 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 34 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 36 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | -------------------------------------------------------------------------------- /api-with-echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | var ( 16 | users []User 17 | instructors []Instructor 18 | courses []Course 19 | ) 20 | 21 | type server struct{} 22 | 23 | // User represent one user of our service 24 | type User struct { 25 | ID int `json:"id"` 26 | Name string `json:"name"` 27 | Email string `json:"email"` 28 | Company string `json:"company"` 29 | Interests []string `json:"interests"` 30 | } 31 | 32 | // Instructor type represent a instructor for a course 33 | type Instructor struct { 34 | ID int `json:"id"` 35 | Name string `json:"name"` 36 | Email string `json:"email"` 37 | Company string `json:"company"` 38 | Expertise []string `json:"expertise"` 39 | } 40 | 41 | // Course is course being taught 42 | type Course struct { 43 | ID int `json:"id"` 44 | InstructorID int `json:"instructor_id"` 45 | Name string `json:"name"` 46 | Topics []string `json:"topics"` 47 | Attendees []int `json:"attendees"` 48 | } 49 | 50 | func init() { 51 | if err := readContent("./data/courses.json", &courses); err != nil { 52 | log.Fatalln("Could not read courses data") 53 | } 54 | if err := readContent("./data/instructors.json", &instructors); err != nil { 55 | log.Fatalln("Could not read instructors data") 56 | } 57 | if err := readContent("./data/users.json", &users); err != nil { 58 | log.Fatalln("Could not read users data") 59 | } 60 | } 61 | 62 | func readContent(filename string, store interface{}) error { 63 | f, err := os.Open(filename) 64 | if err != nil { 65 | return err 66 | } 67 | b, err := ioutil.ReadAll(f) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return json.Unmarshal(b, store) 73 | } 74 | 75 | func contains(in []string, val []string) bool { 76 | found := 0 77 | 78 | for _, n := range in { 79 | n = strings.ToLower(n) 80 | for _, v := range val { 81 | if n == strings.ToLower(v) { 82 | found++ 83 | break 84 | } 85 | } 86 | } 87 | 88 | return len(val) == found 89 | } 90 | 91 | func containsInt(in []int, val []string) bool { 92 | found := 0 93 | for _, _n := range in { 94 | n := strconv.Itoa(_n) 95 | for _, v := range val { 96 | if n == v { 97 | found++ 98 | break 99 | } 100 | } 101 | } 102 | 103 | return len(val) == found 104 | } 105 | 106 | // func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 107 | // w.Header().Add("Content-Type", "application/json") 108 | // e := json.NewEncoder(w) 109 | // e.Encode(s.Routes) 110 | // } 111 | 112 | func getAllUsers(c echo.Context) error { 113 | interests := []string{} 114 | if err := echo.QueryParamsBinder(c).Strings("interest", &interests).BindError(); err != nil { 115 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 116 | } 117 | 118 | res := make([]User, 0) 119 | for _, user := range users { 120 | if contains(user.Interests, interests) { 121 | res = append(res, user) 122 | } 123 | } 124 | 125 | return c.JSON(http.StatusOK, res) 126 | } 127 | 128 | func getAllInstructors(c echo.Context) error { 129 | expertise := []string{} 130 | 131 | // the key was found. 132 | if err := echo.QueryParamsBinder(c).Strings("expertise", &expertise).BindError(); err != nil { //watch the == here 133 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 134 | } 135 | res := make([]Instructor, 0) 136 | for _, instructor := range instructors { 137 | if contains(instructor.Expertise, expertise) { 138 | res = append(res, instructor) 139 | } 140 | } 141 | return c.JSON(http.StatusOK, res) 142 | } 143 | 144 | func getAllCourses(c echo.Context) error { 145 | topics := []string{} 146 | attendees := []string{} 147 | instructor := -1 148 | 149 | if err := echo.QueryParamsBinder(c). 150 | Strings("topic", &topics). 151 | Int("instructor", &instructor). 152 | Strings("attendee", &attendees).BindError(); err != nil { 153 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 154 | } 155 | 156 | res := make([]Course, 0) 157 | for _, course := range courses { 158 | if contains(course.Topics, topics) && containsInt(course.Attendees, attendees) && (instructor == -1 || course.InstructorID == instructor) { 159 | res = append(res, course) 160 | } 161 | } 162 | return c.JSON(http.StatusOK, res) 163 | } 164 | 165 | func getUserByID(c echo.Context) error { 166 | id := -1 167 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 168 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 169 | } 170 | 171 | var data *User 172 | for _, v := range users { 173 | if v.ID == id { 174 | data = &v 175 | break 176 | } 177 | } 178 | 179 | if data == nil { 180 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 181 | } 182 | 183 | return c.JSON(http.StatusOK, data) 184 | } 185 | 186 | func getCoursesByID(c echo.Context) error { 187 | id := -1 188 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 189 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 190 | } 191 | 192 | var data *Course 193 | for _, v := range courses { 194 | if v.ID == id { 195 | data = &v 196 | break 197 | } 198 | } 199 | 200 | if data == nil { 201 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 202 | } 203 | 204 | return c.JSON(http.StatusOK, data) 205 | } 206 | 207 | func getInstructorByID(c echo.Context) error { 208 | id := -1 209 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 210 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 211 | } 212 | 213 | var data *Instructor 214 | for _, v := range instructors { 215 | if v.ID == id { 216 | data = &v 217 | break 218 | } 219 | } 220 | 221 | if data == nil { 222 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 223 | } 224 | 225 | return c.JSON(http.StatusOK, data) 226 | } 227 | 228 | func main() { 229 | e := echo.New() 230 | api := e.Group("/api/v1") 231 | api.GET("/users", getAllUsers) 232 | api.GET("/instructors", getAllInstructors) 233 | api.GET("/courses", getAllCourses) 234 | 235 | api.GET("/users/:id", getUserByID) 236 | api.GET("/instructors/:id", getInstructorByID) 237 | api.GET("/courses/:id", getCoursesByID) 238 | port := "7999" 239 | 240 | e.Logger.Fatal(e.Start(":" + port)) 241 | } 242 | -------------------------------------------------------------------------------- /api-with-gorilla-mux/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/gorillamux 2 | 3 | go 1.14 4 | 5 | require github.com/gorilla/mux v1.7.4 6 | -------------------------------------------------------------------------------- /api-with-gorilla-mux/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 2 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | -------------------------------------------------------------------------------- /api-with-net-http/Readme.md: -------------------------------------------------------------------------------- 1 | # REST API with Standard Library net/http 2 | 3 | ## Run the example 4 | ``` 5 | git checkout origin/standard-library-net-http-07 6 | ``` 7 | 8 | If you are not already in the folder 9 | ``` 10 | cd api-with-net-http 11 | ``` 12 | 13 | ``` 14 | go run main.go 15 | ``` 16 | 17 | ``` 18 | curl localhost:7999 19 | ``` 20 | 21 | You should see output `hello world` printed. 22 | 23 | ## Why a Struct 24 | In our previous example we implemented handler interface on a `anything` type. That worked. So why we are using a `server` struct? 25 | The benefit of using a struct becomes apparent when we build more complex servers. And want to have other methods / types in the struct for instance logger. 26 | 27 | ## Handlerfunc 28 | 29 | The [HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc) type is an adapter to allow the use of ordinary functions as HTTP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f. 30 | 31 | We can pass in any function / method that has the same signature as `ServeHTTP` as a handler. 32 | 33 | ## JSON 34 | For net/http response we can technically write any content we want. But in most cases our data is not read by humans but consumed by other services / applications. So having structured data is a big part of REST. In the olden days `XML` was the go to format for REST but now a days most REST API use `JSON` as the format for the data. 35 | 36 | Go has great support for `JSON` out of the box. 37 | 38 | Once you run this version of the application you can test the output 39 | 40 | ```bash 41 | curl -v localhost:7999/user 42 | ``` 43 | 44 | Your output would looks somehting like this 45 | 46 | ```shell 47 | * Trying ::1... 48 | * TCP_NODELAY set 49 | * Connected to localhost (::1) port 7999 (#0) 50 | > GET /user HTTP/1.1 51 | > Host: localhost:7999 52 | > User-Agent: curl/7.64.1 53 | > Accept: */* 54 | > 55 | < HTTP/1.1 200 OK 56 | < Content-Type: application/json 57 | < Date: Wed, 05 Aug 2020 05:46:31 GMT 58 | < Content-Length: 64 59 | < 60 | {"username":"moficodes","email":"moficodes@gmail.com","age":27} 61 | * Connection #0 to host localhost left intact 62 | * Closing connection 0 63 | ``` 64 | 65 | ## HTTP Verbs 66 | In the previous example we had a method called `getUser`. It is pretty clear it is a GET operation. But if you were to make a request like 67 | 68 | ```bash 69 | curl -x POST "localhost:7999/user 70 | ``` 71 | 72 | You would still see the same output. 73 | 74 | This might seem weird since we had named our function `getUser` we probably was expecting to only do get in that route. But naming our function has no effect on the HTTP verb we allow. 75 | 76 | ```go 77 | http.HandleFunc("/user", s.getUser) 78 | ``` 79 | 80 | This is the line of code that registers that route. And all it says is anytime a request comes we will be serving that using the `s.getUser` HandlerFunc. 81 | 82 | net/http does not have direct support for http verbs like GET, POST, PUT, DELETE etc. But it is easily implementable. This is also a selling poing for other library / framework that is more developer friendly in letting us control the allowed http methods. 83 | 84 | ## REST Routes 85 | In REST the same route can mean different thing based on the HTTP method of the request. 86 | 87 | For example our `/user` route can be a retrieve on `GET` and update on `PUT` 88 | 89 | To test out put 90 | 91 | ``` 92 | curl -X PUT -d '{"username":"mofi","email":"moficodes@ibm.com","age":27}' localhost:7999/user 93 | ``` 94 | 95 | Here we are updating the username to `mofi` and email to `moficodes@ibm.com`. 96 | 97 | We should see `{"update": "ok"}` Print. 98 | 99 | We can also try sending a malformed JSON string. This will return a `405` Bad Request. 100 | 101 | ## Handler vs HandlerFunc vs *HandlerMethod 102 | 103 | > The HandlerMethod is in * because that is not a real term go uses. I am using it to differentiate between a HandlerFunc attached to a type vs a regular function that has the same type as ServeHTTP function. 104 | 105 | The main difference between all of these is developer experience. Creating a new Handler for each of our route is probably not going to be fun. HandlerFunc or methods on structs make is really flexible to build our routes. Choose whichever fits the problem at hand best. For our `/user` endpoint we needed access to a resource that was attached to our `server` struct. So using a `HandlerMethod` was ideal. But if we had a different route that did not have any dependency like that I might have opted for a regular function. 106 | 107 | ## Path Parameter 108 | Matching complex routes using the standard library net/http package is quite difficult. We just added a new route `/base64/` that returns base64 representation anything sent after the `/`. 109 | 110 | ```bash 111 | curl localhost:7999/base64/hello-world 112 | ``` 113 | 114 | We should see output `aGVsbG8td29ybGQ=%` 115 | 116 | Doing anything more complex would be too cumbersome. This is another reason many developers will reach for a library/framework. And if your application has needs for dynamic routes net/http might not be the best solution. 117 | 118 | For example matching `/user/1/blog/4/comment/2` is a very common REST pattern for routes. And with just net/http it will be alot of work to implement something that does this. 119 | 120 | Luckily we have many great libraries and frameworks at our disposal. 121 | 122 | We will move on to gorilla/mux now. 123 | 124 | >The example used here does not follow REST Philosophy. We can have a longer discussion about why something is or isn't restful. The function is more of an action than a representation of some entity. 125 | 126 | ## http default servemux 127 | 128 | In this example we have been using the default serve mux. 129 | 130 | The problem with the default serve mux is that it is a global value which can be changed by any other package. 131 | 132 | To resolve this we can just use a new servemux. 133 | 134 | ```go 135 | mux := http.NewServeMux() 136 | 137 | // other things 138 | 139 | http.ListenAndServe(":7999", mux) 140 | ``` -------------------------------------------------------------------------------- /api-with-net-http/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/stdlib 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /api-with-net-http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | // A REST server has 3 main components 12 | 13 | // Routes 14 | // Route represent the State in REST 15 | // As we know now, REST deals with the represantation of data 16 | // in simplest term, route is anything after the host 17 | // we match these routes in different ways to handle http actions 18 | 19 | // Handlers 20 | // Handlers as the name suggests, handles. 21 | // In go terms Handler is an interface that has only one method: ServeHTTP 22 | // It is any function that takes a ResponseWriter and pointer to a Request 23 | 24 | // Server 25 | // Server is the workhorse for our application 26 | // this is what takes care of incoming request from our client 27 | // It gets many names Mux, ServerMux, Server, Router etc. 28 | 29 | type server struct { 30 | // becasue User is a property of server struct 31 | // we have access to user at all the server instance 32 | User User 33 | } 34 | 35 | type User struct { 36 | // this tags after our struct lets us change the represantation in json 37 | Username string `json:"username"` 38 | Email string `json:"email"` 39 | Age int `json:"age"` 40 | } 41 | 42 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 43 | w.Write([]byte("hello world")) 44 | } 45 | 46 | // 47 | func (s *server) user(w http.ResponseWriter, r *http.Request) { 48 | // just because we are writing JSON does not mean our client will understand 49 | // with this header we make it explicit 50 | w.Header().Add("Content-Type", "application/json") 51 | 52 | switch r.Method { 53 | // if request method is GET business as usual 54 | case "GET": 55 | e := json.NewEncoder(w) 56 | e.Encode(s.User) 57 | case "PUT": 58 | w.Header().Add("Accept", "application/json") 59 | var body User 60 | decoder := json.NewDecoder(r.Body) 61 | err := decoder.Decode(&body) 62 | if err != nil { 63 | w.WriteHeader(http.StatusBadRequest) 64 | return 65 | } 66 | s.User = body 67 | w.WriteHeader(http.StatusOK) 68 | w.Write([]byte(`{"update": "ok"}`)) 69 | // for all other query 70 | // return empty response and 404 status code 71 | default: 72 | w.WriteHeader(http.StatusNotFound) 73 | } 74 | } 75 | 76 | // this handlerFuncs job is simple 77 | // send back whatever was sent to it as base64 encoded 78 | func getBase64(w http.ResponseWriter, r *http.Request) { 79 | message := strings.Split(r.URL.String(), "/")[2] 80 | data := []byte(message) 81 | str := base64.StdEncoding.EncodeToString(data) 82 | w.Write([]byte(str)) 83 | } 84 | 85 | func main() { 86 | mux := http.NewServeMux() 87 | s := &server{ 88 | User: User{ 89 | Username: "moficodes", 90 | Email: "moficodes@gmail.com", 91 | Age: 27, 92 | }, 93 | } 94 | // because s is an instance on server it is now a handler and we can pass it to http.Handle 95 | mux.Handle("/", s) 96 | mux.HandleFunc("/user", s.user) 97 | mux.HandleFunc("/base64/", getBase64) 98 | 99 | port := "7999" 100 | log.Println("starting web server on port", port) 101 | // this is a blocking process 102 | // go will wait for requests to come and program will not exit 103 | log.Fatal(http.ListenAndServe(":"+port, mux)) 104 | } 105 | -------------------------------------------------------------------------------- /middleware-security-echo/Readme.md: -------------------------------------------------------------------------------- 1 | # Middleware and Security 2 | 3 | In this section we will be talking about middleware and security. The topic may seem a little unrelated but implementation wise they go hand in had. 4 | 5 | ## Try it out 6 | 7 | To run the code in this section 8 | 9 | ```bash 10 | git checkout origin/middleware-security-01 11 | ``` 12 | 13 | If you are not already in the folder 14 | 15 | ```bash 16 | cd middleware-security 17 | ``` 18 | 19 | ```bash 20 | go run main.go 21 | ``` 22 | 23 | ```bash 24 | curl localhost:7999 25 | ``` 26 | 27 | ## Middleware 28 | 29 | Middleware is a function that wraps our handler. Thats all. 30 | 31 | ```go 32 | func Middleware(h handler) handler 33 | ``` 34 | 35 | This simple implementation has alot of power. In go functions can be passed in to other functions as a parameter. 36 | 37 | Say we want to add a log to every request we are serving that prints out the URL of the request. 38 | 39 | We can do that by creating our own middleware like so. 40 | 41 | ```go 42 | func Logger(next echo.HandlerFunc) echo.HandlerFunc { 43 | return func(c echo.Context) error { 44 | log.Println(c.Request().URL) 45 | return next(c) 46 | } 47 | } 48 | ``` 49 | 50 | and then using it at the root `echo` router 51 | 52 | ```go 53 | e := echo.New() 54 | e.Use(Logger) 55 | ``` 56 | 57 | On any request made to our server it will now print out the url of request. 58 | 59 | ## Chaining Middleware 60 | 61 | `echo.Use` takes in a slice of middlewares we want to use and apply them in reverse order. 62 | 63 | We can also do it manually ourselves 64 | 65 | ```go 66 | func Chain(h echo.HandlerFunc, middleware ...func(echo.HandlerFunc) echo.HandlerFunc) echo.HandlerFunc { 67 | for _, m := range middleware { 68 | h = m(h) 69 | } 70 | return h 71 | } 72 | ``` 73 | 74 | This is less flexible compared to what echo provides out of the box with `echo.Use`. 75 | 76 | ## Echo Middlewares 77 | 78 | Echo has a list of middlewares built in from the middleware package. This includes CORS, CSRF, JWT, Jaeger, Prometheus and many more. The logger middlerware we used in the last section is also a middleware from echo. You can find the full list at [echo docs](https://echo.labstack.com/middleware/) 79 | 80 | ## JWT 81 | 82 | JSON Web Tokens are an open, industry standard [RFC 7519](https://tools.ietf.org/html/rfc7519) method for representing claims securely between two parties. It is very easy to verify JWT tokens in go. 83 | 84 | In this example we will be validating a JWT token that we generate in [jwt.io](jwt.io) website. With a payload (feel free to use any name or even any other payload here) 85 | 86 | ```json 87 | { 88 | "name": "John Doe" 89 | } 90 | ``` 91 | 92 | And for secret we use a string `very-secret` (goes without saying this is a secret so generate a longer more random string for your application). this will generate us a jwt token. If you dont want to go throught the trouble to generate this yourself, you can use this. 93 | 94 | ``` 95 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.wSzHi09b5o8aSjDHjlGxED9Cg-_-8T6lTWZjs6_Netg 96 | ``` 97 | 98 | We write a new function called `JWTAuth` which is a middleware. In this we check for the Header with key `Authorization`. There is no rule that says token should be sent in this manner. But this in convention and many apps will expect to get the token in this header. So its best practice to keep it there. 99 | 100 | We get the claim and attach it to the request context as extra data so we can get it in our handler when needed. 101 | 102 | In our `handlerFunc` we get the value from context and respond back with the users name. 103 | 104 | ```bash 105 | curl -H "Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.wSzHi09b5o8aSjDHjlGxED9Cg-_-8T6lTWZjs6_Netg" http://localhost:7999/auth/test 106 | ``` 107 | 108 | Our server should respond back with 109 | ```json 110 | { 111 | "data": "John Doe" 112 | } 113 | ``` 114 | 115 | The `jwtCustomClaims` struct embeds `jwt.RegisteredClaims` struct. Which makes `jwtCustomClaims` an implementer of `Claims` interface. Struct embedding is interesting read more about it in [gobyexample](https://gobyexample.com/struct-embedding). 116 | 117 | ```go 118 | type jwtCustomClaims struct { 119 | Name string `json:"name"` 120 | jwt.RegisteredClaims 121 | } 122 | ``` 123 | 124 | This custom claim is being set because we setup echojwt with a config 125 | 126 | ```go 127 | config := echojwt.Config{ 128 | NewClaimsFunc: func(c echo.Context) jwt.Claims { 129 | return new(jwtCustomClaims) 130 | }, 131 | SigningKey: []byte("very-secret"), 132 | } 133 | ``` 134 | 135 | In our `HandlerFunc` we grab the token value set onto our `echo.Context` with key `user`. This is the default value. We could have changed it with the config above if we wanted to. If you want to see how this got set you can take a look at the [implementation here](https://github.com/labstack/echo-jwt/blob/95b0b607987a3bed484870b2052ef212763742a4/jwt.go#L233) 136 | 137 | ```go 138 | func restricted(c echo.Context) error { 139 | user := c.Get("user").(*jwt.Token) 140 | claims := user.Claims.(*jwtCustomClaims) 141 | name := claims.Name 142 | return c.JSON(http.StatusOK, Message{Data: name}) 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /middleware-security-echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/echo-middleware 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/labstack/echo/v4 v4.10.0 8 | ) 9 | 10 | require ( 11 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 12 | github.com/golang-jwt/jwt/v4 v4.4.3 // indirect 13 | github.com/labstack/echo-jwt/v4 v4.1.0 // indirect 14 | github.com/labstack/gommon v0.4.0 // indirect 15 | github.com/mattn/go-colorable v0.1.13 // indirect 16 | github.com/mattn/go-isatty v0.0.17 // indirect 17 | github.com/valyala/bytebufferpool v1.0.0 // indirect 18 | github.com/valyala/fasttemplate v1.2.2 // indirect 19 | golang.org/x/crypto v0.6.0 // indirect 20 | golang.org/x/net v0.6.0 // indirect 21 | golang.org/x/sys v0.5.0 // indirect 22 | golang.org/x/text v0.7.0 // indirect 23 | golang.org/x/time v0.3.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /middleware-security-echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 5 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 6 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 7 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 8 | github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= 9 | github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 10 | github.com/labstack/echo-jwt/v4 v4.1.0 h1:eYGBxauPkyzBM78KJbR5OSz5uhKMDkhJZhTTIuoH6Pg= 11 | github.com/labstack/echo-jwt/v4 v4.1.0/go.mod h1:DHSSaL6cTgczdPXjf8qrTHRbrau2flcddV7CPMs2U/Y= 12 | github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= 13 | github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= 14 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 15 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 16 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 17 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 18 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 19 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 20 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 21 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 22 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 27 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 28 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 29 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 30 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 31 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 32 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 33 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 34 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 35 | golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= 36 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 37 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 42 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 44 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 45 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 46 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 51 | -------------------------------------------------------------------------------- /middleware-security/Readme.md: -------------------------------------------------------------------------------- 1 | # Middleware and Security 2 | 3 | In this section we will be talking about middleware and security. The topic may seem a little unrelated but implementation wise they go hand in had. 4 | 5 | ## Try it out 6 | 7 | To run the code in this section 8 | 9 | ```bash 10 | git checkout origin/middleware-security-04 11 | ``` 12 | 13 | If you are not already in the folder 14 | 15 | ```bash 16 | cd middleware-security 17 | ``` 18 | 19 | ```bash 20 | go run main.go 21 | ``` 22 | 23 | ```bash 24 | curl localhost:7999 25 | ``` 26 | 27 | ## Middleware 28 | 29 | Middleware is a function that wraps our handler. Thats all. 30 | 31 | ```go 32 | func Middleware(h handler) handler 33 | ``` 34 | 35 | This simple implementation has alot of power. In go functions can be passed in to other functions as a parameter. 36 | 37 | Say we want to add a log to every request we are serving that prints out the URL of the request. 38 | 39 | We can do that by creating our own middleware like so. 40 | 41 | ```go 42 | func Logger(next http.Handler) http.Handler { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | log.Println(r.URL) 45 | next.ServeHTTP(w, r) 46 | }) 47 | } 48 | ``` 49 | 50 | and then wrapping our main router 51 | 52 | ```go 53 | log.Fatal(http.ListenAndServe(":"+port, Logger(r))) 54 | ``` 55 | 56 | On any request made to our server it will now print out the url of request on every request. 57 | 58 | ## Chaining Middlewares 59 | 60 | Middlewares are useful tools and often one is not enough. If we want to add more of these middlewares we can keep on wrapping our router with our middlewares. 61 | 62 | For example 63 | 64 | ```go 65 | ...SomeOtherMiddlerware(OtherMiddlerWare(Logger(r))) 66 | ``` 67 | 68 | There is couple of other ways to do this. 69 | 70 | ```go 71 | // Chain applies middlewares to a http.HandlerFunc 72 | func Chain(f http.Handler, middlewares ...func(next http.Handler) http.Handler) http.Handler { 73 | for _, m := range middlewares { 74 | f = m(f) 75 | } 76 | return f 77 | } 78 | ``` 79 | 80 | Then we can use our middlewares like this, 81 | 82 | ```go 83 | Chain(r, Logger, OtherMiddlerWare, SomeOtherMiddlerware, ...) 84 | ``` 85 | 86 | This in my opinion is a bit cleaner implementation. 87 | 88 | In gorilla mux we have another option to do this. 89 | 90 | Router has a method `Use` that takes an array of Middlewares. This is useful to add middlewares to subrouters. 91 | 92 | ## Mux Handlers 93 | 94 | Gorilla mux has a module named handlers. Its a collection of useful middleware handlers for gorilla mux. 95 | 96 | We can make use of one of these middlewares to make a better logger for our routes. 97 | 98 | ```go 99 | func MuxLogger(next http.Handler) http.Handler { 100 | return handlers.LoggingHandler(os.Stdout, next) 101 | } 102 | ``` 103 | 104 | We add this middleware in our chain 105 | 106 | ```go 107 | log.Fatal(http.ListenAndServe(":"+port, Chain(r, MuxLogger, Logger))) 108 | ``` 109 | 110 | And the output we get is, 111 | 112 | ```bash 113 | 2020/08/07 03:44:59 /api/v1/users 114 | 2020/08/07 03:44:59 /api/v1/users 901.821µs 115 | ::1 - - [07/Aug/2020:03:44:59 -0400] "GET /api/v1/users HTTP/1.1" 200 96107 116 | ``` 117 | 118 | The first line comes from our `Logger` middleware. 119 | The second line is from the `Time` middleware. 120 | And finally the third line is from the `MuxLogger` that is using the `LoggingHandler` middleware from mux. 121 | 122 | >The mux Logger middleware gets the http status code of the response being sent out. If we wanted, we could have implemented something like that ourselves. We would need to use a http hijacker and a custop response writer. 123 | 124 | ## JWT Authentication 125 | 126 | JSON Web Tokens are an open, industry standard [RFC 7519](https://tools.ietf.org/html/rfc7519) method for representing claims securely between two parties. It is very easy to verify JWT tokens in go. 127 | 128 | We make use of the very popular [jwt-go](https://godoc.org/github.com/dgrijalva/jwt-go#example-Parse--Hmac) library to validate a JWT Token. 129 | 130 | In this example we will be validating a JWT token that we generate in [jwt.io](jwt.io) website. With a payload (feel free to use any name or even any other payload here) 131 | 132 | ```json 133 | { 134 | "name": "John Doe" 135 | } 136 | ``` 137 | 138 | And for secret we use a string `very-secret` (goes without saying this is a secret so generate a longer more random string for your application). this will generate us a jwt token. If you dont want to go throught the trouble to generate this yourself, you can use this. 139 | 140 | ``` 141 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.wSzHi09b5o8aSjDHjlGxED9Cg-_-8T6lTWZjs6_Netg 142 | ``` 143 | 144 | We write a new function called `JWTAuth` which is a middleware. In this we check for the Header with key `Authorization`. There is no rule that says token should be sent in this manner. But this in convention and many apps will expect to get the token in this header. So its best practice to keep it there. 145 | 146 | We get the claim and attach it to the request context as extra data so we can get it in our handler when needed. 147 | 148 | In our `handlerFunc` we get the value from context and respond back with the users name. 149 | 150 | ```bash 151 | curl -H "Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.wSzHi09b5o8aSjDHjlGxED9Cg-_-8T6lTWZjs6_Netg" http://localhost:7999/auth/test 152 | ``` 153 | 154 | Our server should respond back with 155 | ```json 156 | { 157 | "message": "hello John Doe" 158 | } 159 | ``` 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /middleware-security/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/middleware 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gorilla/handlers v1.4.2 8 | github.com/gorilla/mux v1.7.4 9 | ) 10 | -------------------------------------------------------------------------------- /middleware-security/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 4 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | -------------------------------------------------------------------------------- /project-structure/Readme.md: -------------------------------------------------------------------------------- 1 | # Project Structure 2 | 3 | In this chapter we will discuss project structure. In go project structure does not make any difference in performance or functionality. It is a matter of preference and organization of code. 4 | 5 | ## Flat Structure 6 | 7 | As the name suggest in this structure the code is laid out in a single folder. This structure is great to start out as everything is accessible everywhere. I recommend starting with this. 8 | 9 | ``` 10 | rest-api 11 | ├── Readme.yaml 12 | ├── data 13 | │ ├── courses.json 14 | │ ├── instructors.json 15 | │ └── users.json 16 | ├── go.mod 17 | ├── go.sum 18 | ├── handlers.go 19 | ├── main.go 20 | ├── middlewares.go 21 | └── types.go 22 | ``` 23 | 24 | Our Course API was already in a flat structure. But better organized version can be seen [here](https://github.com/moficodes/restful-go-api/tree/project-structure-01/project-structure/flat-structure). 25 | 26 | ## MVC Structure 27 | 28 | This structure follows the MVC pattern to separate out Model, Controller and Data layer. If you are coming from a MVC structure project from languages like C#, Java etc this might be more comfortable to start with. 29 | 30 | ``` 31 | rest-api 32 | ├── Readme.md 33 | ├── model 34 | └── user.go 35 | ├── controller 36 | └── user.go 37 | ├── go.mod 38 | ├── go.sum 39 | └── main.go 40 | ``` 41 | 42 | ## Pkg Structure 43 | 44 | This is a very common pattern in the go ecosystem. 45 | 46 | ``` 47 | rest-api 48 | pkg-structure 49 | ├── Readme.md 50 | ├── cmd 51 | │ └── web 52 | │ └── main.go 53 | ├── data 54 | │ ├── courses.json 55 | │ ├── instructors.json 56 | │ └── users.json 57 | ├── go.mod 58 | ├── go.sum 59 | ├── internal 60 | │ ├── handler 61 | │ │ ├── authenticated.go 62 | │ │ ├── course.go 63 | │ │ ├── instructor.go 64 | │ │ ├── server.go 65 | │ │ ├── types.go 66 | │ │ ├── user.go 67 | │ │ └── utils.go 68 | │ └── middleware 69 | │ ├── jwt.go 70 | │ └── types.go 71 | └── pkg 72 | └── middleware 73 | └── logger.go 74 | ``` 75 | 76 | Our Course API is converted in the pkg structure [here](https://github.com/moficodes/restful-go-api/tree/project-structure-01/project-structure/pkg-structure). 77 | 78 | _What to put in pkg vs internals?_ 79 | 80 | > The way I reason about it is, the things in pkg could be used for other projects just by copying the code or directly importing as a module. Internal has logic that only pertain to this application. 81 | 82 | I have using this structure for my project recently. One great benefit is that, this structure promotes the idea of multiple binary from the same code base. Inside my `cmd` folder I could easily create `cli` binary that uses the same underlying code to create a CLI tool. Not to say it is impossible to do so in the other structure. But this structure suits that usecase perfectly. 83 | 84 | _Which one should I choose?_ 85 | 86 | > Like most things in technology the answer is "It depends". If you are starting a new project you are pretty much free to structure your code in any way you like. 87 | 88 | -------------------------------------------------------------------------------- /project-structure/flat-structure/Readme.md: -------------------------------------------------------------------------------- 1 | # Flat Structure 2 | 3 | ## Try it out 4 | 5 | To run the code in this section 6 | 7 | ```bash 8 | git checkout origin/project-structure-01 9 | ``` 10 | 11 | If you are not already in the folder 12 | ``` 13 | cd project-structure/flat-structure 14 | ``` 15 | 16 | ``` 17 | go run . 18 | ``` 19 | > Becaus everything at the top level, just running `main.go` is not enough. with `.` we let go builder know to include all the go file it can find. 20 | 21 | ```bash 22 | curl localhost:7999 23 | ``` 24 | -------------------------------------------------------------------------------- /project-structure/flat-structure/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/middleware 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gorilla/handlers v1.4.2 8 | github.com/gorilla/mux v1.7.4 9 | ) 10 | -------------------------------------------------------------------------------- /project-structure/flat-structure/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 4 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | -------------------------------------------------------------------------------- /project-structure/flat-structure/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/dgrijalva/jwt-go" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15 | w.Header().Add("Content-Type", "application/json") 16 | e := json.NewEncoder(w) 17 | e.Encode(s.Routes) 18 | } 19 | 20 | func getAllUsers(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Add("Content-Type", "application/json") 22 | 23 | query := r.URL.Query() 24 | interests, ok := query["interest"] 25 | // the key was found. 26 | if ok { 27 | res := make([]User, 0) 28 | for _, user := range users { 29 | if contains(user.Interests, interests) { 30 | res = append(res, user) 31 | } 32 | } 33 | 34 | e := json.NewEncoder(w) 35 | e.Encode(res) 36 | return 37 | } 38 | 39 | e := json.NewEncoder(w) 40 | e.Encode(users) 41 | } 42 | 43 | func getAllInstructors(w http.ResponseWriter, r *http.Request) { 44 | w.Header().Add("Content-Type", "application/json") 45 | 46 | query := r.URL.Query() 47 | expertise, ok := query["expertise"] 48 | // the key was found. 49 | if ok { 50 | res := make([]Instructor, 0) 51 | for _, instructor := range instructors { 52 | if contains(instructor.Expertise, expertise) { 53 | res = append(res, instructor) 54 | } 55 | } 56 | 57 | e := json.NewEncoder(w) 58 | e.Encode(res) 59 | return 60 | } 61 | 62 | e := json.NewEncoder(w) 63 | e.Encode(instructors) 64 | } 65 | 66 | func getAllCourses(w http.ResponseWriter, r *http.Request) { 67 | w.Header().Add("Content-Type", "application/json") 68 | query := r.URL.Query() 69 | topics, ok := query["topic"] 70 | if ok { 71 | res := make([]Course, 0) 72 | for _, course := range courses { 73 | if contains(course.Topics, topics) { 74 | res = append(res, course) 75 | } 76 | } 77 | 78 | e := json.NewEncoder(w) 79 | e.Encode(res) 80 | return 81 | } 82 | 83 | e := json.NewEncoder(w) 84 | e.Encode(courses) 85 | } 86 | 87 | func getCoursesWithInstructorAndAttendee(w http.ResponseWriter, r *http.Request) { 88 | w.Header().Add("Content-Type", "application/json") 89 | // we don't have to check for multiple instructor because the way our data is structured 90 | // there is no way multiple instructor can be part of same course 91 | _instructor := r.URL.Query().Get("instructor") 92 | instructorID, _ := strconv.Atoi(_instructor) 93 | // but multiple attendee can be part of the same course 94 | // since we gurrantee only valid integer queries will be sent to this route 95 | // we don't need to check if there is value or not. 96 | attendees := r.URL.Query()["attendee"] 97 | res := make([]Course, 0) 98 | 99 | for _, course := range courses { 100 | if course.InstructorID == instructorID && containsInt(course.Attendees, attendees) { 101 | res = append(res, course) 102 | } 103 | } 104 | 105 | e := json.NewEncoder(w) 106 | e.Encode(res) 107 | } 108 | 109 | func getUserByID(w http.ResponseWriter, r *http.Request) { 110 | w.Header().Add("Content-Type", "application/json") 111 | pathParams := mux.Vars(r) 112 | id := -1 113 | var err error 114 | if val, ok := pathParams["id"]; ok { 115 | id, err = strconv.Atoi(val) 116 | if err != nil { 117 | w.WriteHeader(http.StatusBadRequest) 118 | w.Write([]byte(`{"error": "need a valid id"}`)) 119 | return 120 | } 121 | } 122 | 123 | var data *User 124 | for _, v := range users { 125 | if v.ID == id { 126 | data = &v 127 | break 128 | } 129 | } 130 | 131 | if data == nil { 132 | w.WriteHeader(http.StatusNotFound) 133 | w.Write([]byte(`{"error": "not found"}`)) 134 | } 135 | 136 | e := json.NewEncoder(w) 137 | e.Encode(data) 138 | } 139 | 140 | func getCoursesByID(w http.ResponseWriter, r *http.Request) { 141 | w.Header().Add("Content-Type", "application/json") 142 | // this function takes in the request and parses 143 | // all the pathParams from it 144 | // pathParams is a map[string]string 145 | pathParams := mux.Vars(r) 146 | id := -1 147 | var err error 148 | if val, ok := pathParams["id"]; ok { 149 | id, err = strconv.Atoi(val) 150 | if err != nil { 151 | w.WriteHeader(http.StatusBadRequest) 152 | w.Write([]byte(`{"error": "need a valid id"}`)) 153 | return 154 | } 155 | } 156 | 157 | var data *Course 158 | for _, v := range courses { 159 | if v.ID == id { 160 | data = &v 161 | break 162 | } 163 | } 164 | 165 | if data == nil { 166 | w.WriteHeader(http.StatusNotFound) 167 | w.Write([]byte(`{"error": "not found"}`)) 168 | } 169 | 170 | e := json.NewEncoder(w) 171 | e.Encode(data) 172 | } 173 | 174 | func getInstructorByID(w http.ResponseWriter, r *http.Request) { 175 | w.Header().Add("Content-Type", "application/json") 176 | pathParams := mux.Vars(r) 177 | id := -1 178 | var err error 179 | if val, ok := pathParams["id"]; ok { 180 | id, err = strconv.Atoi(val) 181 | if err != nil { 182 | w.WriteHeader(http.StatusBadRequest) 183 | w.Write([]byte(`{"error": "need a valid id"}`)) 184 | return 185 | } 186 | } 187 | 188 | var data *Instructor 189 | for _, v := range instructors { 190 | if v.ID == id { 191 | data = &v 192 | break 193 | } 194 | } 195 | 196 | if data == nil { 197 | w.WriteHeader(http.StatusNotFound) 198 | w.Write([]byte(`{"error": "not found"}`)) 199 | } 200 | 201 | e := json.NewEncoder(w) 202 | e.Encode(data) 203 | } 204 | 205 | func authHandler(w http.ResponseWriter, r *http.Request) { 206 | data := r.Context().Value(ContextKey("props")).(jwt.MapClaims) 207 | 208 | name, ok := data["name"] 209 | if !ok { 210 | w.WriteHeader(http.StatusUnauthorized) 211 | w.Write([]byte(`{"error": "not authorized"}`)) 212 | return 213 | } 214 | w.WriteHeader(http.StatusOK) 215 | w.Write([]byte(fmt.Sprintf("{\"message\": \"hello %v\"}", name))) 216 | } 217 | 218 | func contains(in []string, val []string) bool { 219 | found := 0 220 | 221 | for _, n := range in { 222 | n = strings.ToLower(n) 223 | for _, v := range val { 224 | if n == strings.ToLower(v) { 225 | found++ 226 | break 227 | } 228 | } 229 | } 230 | 231 | return len(val) == found 232 | } 233 | 234 | func containsInt(in []int, val []string) bool { 235 | found := 0 236 | for _, _n := range in { 237 | n := strconv.Itoa(_n) 238 | for _, v := range val { 239 | if n == v { 240 | found++ 241 | break 242 | } 243 | } 244 | } 245 | 246 | return len(val) == found 247 | } 248 | -------------------------------------------------------------------------------- /project-structure/flat-structure/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | var ( 14 | users []User 15 | instructors []Instructor 16 | courses []Course 17 | ) 18 | 19 | func init() { 20 | if err := readContent("./data/courses.json", &courses); err != nil { 21 | log.Fatalln("Could not read courses data") 22 | } 23 | if err := readContent("./data/instructors.json", &instructors); err != nil { 24 | log.Fatalln("Could not read instructors data") 25 | } 26 | if err := readContent("./data/users.json", &users); err != nil { 27 | log.Fatalln("Could not read users data") 28 | } 29 | } 30 | 31 | func readContent(filename string, store interface{}) error { 32 | f, err := os.Open(filename) 33 | if err != nil { 34 | return err 35 | } 36 | b, err := ioutil.ReadAll(f) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return json.Unmarshal(b, store) 42 | } 43 | 44 | func main() { 45 | s := &server{Routes: make([]string, 0)} 46 | r := mux.NewRouter() 47 | 48 | r.Handle("/", s) 49 | api := r.PathPrefix("/api/v1").Subrouter() 50 | auth := r.PathPrefix("/auth").Subrouter() 51 | auth.Use(JWTAuth) 52 | auth.HandleFunc("/check", authHandler) 53 | 54 | api.Use(Time) 55 | 56 | api.HandleFunc("/users", getAllUsers).Methods(http.MethodGet) 57 | 58 | api.HandleFunc("/courses", getCoursesWithInstructorAndAttendee). 59 | Queries("instructor", "{instructor:[0-9]+}", "attendee", "{attendee:[0-9]+}"). 60 | Methods(http.MethodGet) 61 | 62 | api.HandleFunc("/courses", getAllCourses).Methods(http.MethodGet) 63 | api.HandleFunc("/instructors", getAllInstructors).Methods(http.MethodGet) 64 | 65 | // in gorilla mux we can name path parameters 66 | // the library will put them in an key,val map for us 67 | api.HandleFunc("/users/{id}", getUserByID).Methods(http.MethodGet) 68 | api.HandleFunc("/courses/{id}", getUserByID).Methods(http.MethodGet) 69 | api.HandleFunc("/instructors/{id}", getUserByID).Methods(http.MethodGet) 70 | 71 | port := "7999" 72 | log.Println("starting web server on port", port) 73 | 74 | r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 75 | t, err := route.GetPathTemplate() 76 | if err != nil { 77 | return err 78 | } 79 | s.Routes = append(s.Routes, t) 80 | return nil 81 | }) 82 | log.Println("available routes: ", s.Routes) 83 | // instead of using the default handler that comes with net/http we use the mux router from gorilla mux 84 | log.Fatal(http.ListenAndServe(":"+port, Chain(r, MuxLogger, Logger))) 85 | } 86 | -------------------------------------------------------------------------------- /project-structure/flat-structure/middlewares.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/dgrijalva/jwt-go" 13 | "github.com/gorilla/handlers" 14 | ) 15 | 16 | func Chain(f http.Handler, 17 | middlewares ...func(next http.Handler) http.Handler) http.Handler { 18 | for _, m := range middlewares { 19 | f = m(f) 20 | } 21 | return f 22 | } 23 | 24 | func Logger(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | log.Println(r.URL) 27 | next.ServeHTTP(w, r) 28 | }) 29 | } 30 | 31 | func MuxLogger(next http.Handler) http.Handler { 32 | return handlers.LoggingHandler(os.Stdout, next) 33 | } 34 | 35 | func Time(next http.Handler) http.Handler { 36 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | start := time.Now() 38 | defer func() { log.Println(r.URL.Path, time.Since(start)) }() 39 | next.ServeHTTP(w, r) 40 | }) 41 | } 42 | 43 | func JWTAuth(next http.Handler) http.Handler { 44 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | authorization := r.Header.Get("Authorization") 46 | // auth token have the structure `bearer ` 47 | // so we split it on the ` ` (space character) 48 | splitToken := strings.Split(authorization, " ") 49 | // if we end up with a array of size 2 we have the token as the 50 | // 2nd item in the array 51 | if len(splitToken) != 2 { 52 | // we got something different 53 | w.WriteHeader(http.StatusUnauthorized) 54 | w.Write([]byte(`{"error": "not authorized"}`)) 55 | return 56 | } 57 | // second item is our possible token 58 | jwtToken := splitToken[1] 59 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 60 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 61 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 62 | } 63 | return []byte("very-secret"), nil 64 | }) 65 | 66 | if err != nil { 67 | // we got something different 68 | w.WriteHeader(http.StatusUnauthorized) 69 | w.Write([]byte(`{"error": "not authorized"}`)) 70 | return 71 | } 72 | 73 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 74 | ctx := context.WithValue(r.Context(), ContextKey("props"), claims) 75 | next.ServeHTTP(w, r.WithContext(ctx)) 76 | } else { 77 | w.WriteHeader(http.StatusUnauthorized) 78 | w.Write([]byte(`{"error": "not authorized"}`)) 79 | } 80 | 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /project-structure/flat-structure/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // ContextKey is not primitive type key for context 4 | type ContextKey string 5 | 6 | type server struct { 7 | Routes []string `json:"routes"` 8 | } 9 | 10 | // User represent one user of our service 11 | type User struct { 12 | ID int `json:"id"` 13 | Name string `json:"name"` 14 | Email string `json:"email"` 15 | Company string `json:"company"` 16 | Interests []string `json:"interests"` 17 | } 18 | 19 | // Instructor type represent a instructor for a course 20 | type Instructor struct { 21 | ID int `json:"id"` 22 | Name string `json:"name"` 23 | Email string `json:"email"` 24 | Company string `json:"company"` 25 | Expertise []string `json:"expertise"` 26 | } 27 | 28 | // Course is course being taught 29 | type Course struct { 30 | ID int `json:"id"` 31 | InstructorID int `json:"instructor_id"` 32 | Name string `json:"name"` 33 | Topics []string `json:"topics"` 34 | Attendees []int `json:"attendees"` 35 | } 36 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/Readme.md: -------------------------------------------------------------------------------- 1 | # Flat Structure 2 | 3 | ## Try it out 4 | 5 | To run the code in this section 6 | 7 | ```bash 8 | git checkout origin/project-structure-01 9 | ``` 10 | 11 | If you are not already in the folder 12 | ``` 13 | cd project-structure/pkg-structure 14 | ``` 15 | 16 | ``` 17 | go run cmd/web/main.go 18 | ``` 19 | 20 | ```bash 21 | curl localhost:7999 22 | ``` 23 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | 9 | "github.com/moficodes/restful-go-api/pkg/internal/handler" 10 | internalMiddleware "github.com/moficodes/restful-go-api/pkg/internal/middleware" 11 | "github.com/moficodes/restful-go-api/pkg/pkg/middleware" 12 | ) 13 | 14 | func main() { 15 | s := &handler.Server{Routes: make([]string, 0)} 16 | r := mux.NewRouter() 17 | 18 | r.Handle("/", s) 19 | api := r.PathPrefix("/api/v1").Subrouter() 20 | auth := r.PathPrefix("/auth").Subrouter() 21 | auth.Use(internalMiddleware.JWTAuth) 22 | auth.HandleFunc("/check", handler.AuthHandler) 23 | 24 | api.Use(middleware.Time) 25 | 26 | api.HandleFunc("/users", handler.GetAllUsers).Methods(http.MethodGet) 27 | 28 | api.HandleFunc("/courses", handler.GetCoursesWithInstructorAndAttendee). 29 | Queries("instructor", "{instructor:[0-9]+}", "attendee", "{attendee:[0-9]+}"). 30 | Methods(http.MethodGet) 31 | 32 | api.HandleFunc("/courses", handler.GetAllCourses).Methods(http.MethodGet) 33 | api.HandleFunc("/instructors", handler.GetAllInstructors).Methods(http.MethodGet) 34 | 35 | // in gorilla mux we can name path parameters 36 | // the library will put them in an key,val map for us 37 | api.HandleFunc("/users/{id}", handler.GetUserByID).Methods(http.MethodGet) 38 | api.HandleFunc("/courses/{id}", handler.GetCoursesByID).Methods(http.MethodGet) 39 | api.HandleFunc("/instructors/{id}", handler.GetInstructorByID).Methods(http.MethodGet) 40 | 41 | port := "7999" 42 | log.Println("starting web server on port", port) 43 | 44 | r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 45 | t, err := route.GetPathTemplate() 46 | if err != nil { 47 | return err 48 | } 49 | s.Routes = append(s.Routes, t) 50 | return nil 51 | }) 52 | log.Println("available routes: ", s.Routes) 53 | // instead of using the default handler that comes with net/http we use the mux router from gorilla mux 54 | log.Fatal(http.ListenAndServe(":"+port, middleware.Chain(r, middleware.MuxLogger, middleware.Logger))) 55 | } 56 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/pkg 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gorilla/handlers v1.4.2 8 | github.com/gorilla/mux v1.7.4 9 | ) 10 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 4 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/moficodes/restful-go-api/pkg/internal/middleware" 9 | ) 10 | 11 | func AuthHandler(w http.ResponseWriter, r *http.Request) { 12 | data := r.Context().Value(middleware.ContextKey("props")).(jwt.MapClaims) 13 | 14 | name, ok := data["name"] 15 | if !ok { 16 | w.WriteHeader(http.StatusUnauthorized) 17 | w.Write([]byte(`{"error": "not authorized"}`)) 18 | return 19 | } 20 | w.WriteHeader(http.StatusOK) 21 | w.Write([]byte(fmt.Sprintf("{\"message\": \"hello %v\"}", name))) 22 | } 23 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/course.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | courses []Course 14 | ) 15 | 16 | func init() { 17 | if err := readContent("./data/courses.json", &courses); err != nil { 18 | log.Fatalln("Could not read courses data") 19 | } 20 | } 21 | 22 | func GetAllCourses(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Add("Content-Type", "application/json") 24 | query := r.URL.Query() 25 | topics, ok := query["topic"] 26 | if ok { 27 | res := make([]Course, 0) 28 | for _, course := range courses { 29 | if contains(course.Topics, topics) { 30 | res = append(res, course) 31 | } 32 | } 33 | 34 | e := json.NewEncoder(w) 35 | e.Encode(res) 36 | return 37 | } 38 | 39 | e := json.NewEncoder(w) 40 | e.Encode(courses) 41 | } 42 | 43 | func GetCoursesWithInstructorAndAttendee(w http.ResponseWriter, r *http.Request) { 44 | w.Header().Add("Content-Type", "application/json") 45 | // we don't have to check for multiple instructor because the way our data is structured 46 | // there is no way multiple instructor can be part of same course 47 | _instructor := r.URL.Query().Get("instructor") 48 | instructorID, _ := strconv.Atoi(_instructor) 49 | // but multiple attendee can be part of the same course 50 | // since we gurrantee only valid integer queries will be sent to this route 51 | // we don't need to check if there is value or not. 52 | attendees := r.URL.Query()["attendee"] 53 | res := make([]Course, 0) 54 | 55 | for _, course := range courses { 56 | if course.InstructorID == instructorID && containsInt(course.Attendees, attendees) { 57 | res = append(res, course) 58 | } 59 | } 60 | 61 | e := json.NewEncoder(w) 62 | e.Encode(res) 63 | } 64 | 65 | func GetCoursesByID(w http.ResponseWriter, r *http.Request) { 66 | w.Header().Add("Content-Type", "application/json") 67 | // this function takes in the request and parses 68 | // all the pathParams from it 69 | // pathParams is a map[string]string 70 | pathParams := mux.Vars(r) 71 | id := -1 72 | var err error 73 | if val, ok := pathParams["id"]; ok { 74 | id, err = strconv.Atoi(val) 75 | if err != nil { 76 | w.WriteHeader(http.StatusBadRequest) 77 | w.Write([]byte(`{"error": "need a valid id"}`)) 78 | return 79 | } 80 | } 81 | 82 | var data *Course 83 | for _, v := range courses { 84 | if v.ID == id { 85 | data = &v 86 | break 87 | } 88 | } 89 | 90 | if data == nil { 91 | w.WriteHeader(http.StatusNotFound) 92 | w.Write([]byte(`{"error": "not found"}`)) 93 | } 94 | 95 | e := json.NewEncoder(w) 96 | e.Encode(data) 97 | } 98 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/instructor.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | instructors []Instructor 14 | ) 15 | 16 | func init() { 17 | if err := readContent("./data/instructors.json", &instructors); err != nil { 18 | log.Fatalln("Could not read instructors data") 19 | } 20 | } 21 | 22 | func GetAllInstructors(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Add("Content-Type", "application/json") 24 | 25 | query := r.URL.Query() 26 | expertise, ok := query["expertise"] 27 | // the key was found. 28 | if ok { 29 | res := make([]Instructor, 0) 30 | for _, instructor := range instructors { 31 | if contains(instructor.Expertise, expertise) { 32 | res = append(res, instructor) 33 | } 34 | } 35 | 36 | e := json.NewEncoder(w) 37 | e.Encode(res) 38 | return 39 | } 40 | 41 | e := json.NewEncoder(w) 42 | e.Encode(instructors) 43 | } 44 | 45 | func GetInstructorByID(w http.ResponseWriter, r *http.Request) { 46 | w.Header().Add("Content-Type", "application/json") 47 | pathParams := mux.Vars(r) 48 | id := -1 49 | var err error 50 | if val, ok := pathParams["id"]; ok { 51 | id, err = strconv.Atoi(val) 52 | if err != nil { 53 | w.WriteHeader(http.StatusBadRequest) 54 | w.Write([]byte(`{"error": "need a valid id"}`)) 55 | return 56 | } 57 | } 58 | 59 | var data *Instructor 60 | for _, v := range instructors { 61 | if v.ID == id { 62 | data = &v 63 | break 64 | } 65 | } 66 | 67 | if data == nil { 68 | w.WriteHeader(http.StatusNotFound) 69 | w.Write([]byte(`{"error": "not found"}`)) 70 | } 71 | 72 | e := json.NewEncoder(w) 73 | e.Encode(data) 74 | } 75 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/server.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 9 | w.Header().Add("Content-Type", "application/json") 10 | e := json.NewEncoder(w) 11 | e.Encode(s.Routes) 12 | } 13 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/types.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | type Server struct { 4 | Routes []string `json:"routes"` 5 | } 6 | 7 | // User represent one user of our service 8 | type User struct { 9 | ID int `json:"id"` 10 | Name string `json:"name"` 11 | Email string `json:"email"` 12 | Company string `json:"company"` 13 | Interests []string `json:"interests"` 14 | } 15 | 16 | // Instructor type represent a instructor for a course 17 | type Instructor struct { 18 | ID int `json:"id"` 19 | Name string `json:"name"` 20 | Email string `json:"email"` 21 | Company string `json:"company"` 22 | Expertise []string `json:"expertise"` 23 | } 24 | 25 | // Course is course being taught 26 | type Course struct { 27 | ID int `json:"id"` 28 | InstructorID int `json:"instructor_id"` 29 | Name string `json:"name"` 30 | Topics []string `json:"topics"` 31 | Attendees []int `json:"attendees"` 32 | } 33 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | users []User 14 | ) 15 | 16 | func init() { 17 | if err := readContent("./data/users.json", &users); err != nil { 18 | log.Fatalln("Could not read users data") 19 | } 20 | } 21 | 22 | func GetAllUsers(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Add("Content-Type", "application/json") 24 | 25 | query := r.URL.Query() 26 | interests, ok := query["interest"] 27 | // the key was found. 28 | if ok { 29 | res := make([]User, 0) 30 | for _, user := range users { 31 | if contains(user.Interests, interests) { 32 | res = append(res, user) 33 | } 34 | } 35 | 36 | e := json.NewEncoder(w) 37 | e.Encode(res) 38 | return 39 | } 40 | 41 | e := json.NewEncoder(w) 42 | e.Encode(users) 43 | } 44 | 45 | func GetUserByID(w http.ResponseWriter, r *http.Request) { 46 | w.Header().Add("Content-Type", "application/json") 47 | pathParams := mux.Vars(r) 48 | id := -1 49 | var err error 50 | if val, ok := pathParams["id"]; ok { 51 | id, err = strconv.Atoi(val) 52 | if err != nil { 53 | w.WriteHeader(http.StatusBadRequest) 54 | w.Write([]byte(`{"error": "need a valid id"}`)) 55 | return 56 | } 57 | } 58 | 59 | var data *User 60 | for _, v := range users { 61 | if v.ID == id { 62 | data = &v 63 | break 64 | } 65 | } 66 | 67 | if data == nil { 68 | w.WriteHeader(http.StatusNotFound) 69 | w.Write([]byte(`{"error": "not found"}`)) 70 | } 71 | 72 | e := json.NewEncoder(w) 73 | e.Encode(data) 74 | } 75 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/handler/utils.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | ) 11 | 12 | func JWTAuth(next http.Handler) http.Handler { 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | authorization := r.Header.Get("Authorization") 15 | // auth token have the structure `bearer ` 16 | // so we split it on the ` ` (space character) 17 | splitToken := strings.Split(authorization, " ") 18 | // if we end up with a array of size 2 we have the token as the 19 | // 2nd item in the array 20 | if len(splitToken) != 2 { 21 | // we got something different 22 | w.WriteHeader(http.StatusUnauthorized) 23 | w.Write([]byte(`{"error": "not authorized"}`)) 24 | return 25 | } 26 | // second item is our possible token 27 | jwtToken := splitToken[1] 28 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 29 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 30 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 31 | } 32 | return []byte("very-secret"), nil 33 | }) 34 | 35 | if err != nil { 36 | // we got something different 37 | w.WriteHeader(http.StatusUnauthorized) 38 | w.Write([]byte(`{"error": "not authorized"}`)) 39 | return 40 | } 41 | 42 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 43 | ctx := context.WithValue(r.Context(), ContextKey("props"), claims) 44 | next.ServeHTTP(w, r.WithContext(ctx)) 45 | } else { 46 | w.WriteHeader(http.StatusUnauthorized) 47 | w.Write([]byte(`{"error": "not authorized"}`)) 48 | } 49 | 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/internal/middleware/types.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | // ContextKey is not primitive type key for context 4 | type ContextKey string 5 | -------------------------------------------------------------------------------- /project-structure/pkg-structure/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/gorilla/handlers" 10 | ) 11 | 12 | func Chain(f http.Handler, 13 | middlewares ...func(next http.Handler) http.Handler) http.Handler { 14 | for _, m := range middlewares { 15 | f = m(f) 16 | } 17 | return f 18 | } 19 | 20 | func Logger(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | log.Println(r.URL) 23 | next.ServeHTTP(w, r) 24 | }) 25 | } 26 | 27 | func MuxLogger(next http.Handler) http.Handler { 28 | return handlers.LoggingHandler(os.Stdout, next) 29 | } 30 | 31 | func Time(next http.Handler) http.Handler { 32 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | start := time.Now() 34 | defer func() { log.Println(r.URL.Path, time.Since(start)) }() 35 | next.ServeHTTP(w, r) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /rest-api-container/.env.example: -------------------------------------------------------------------------------- 1 | DB_USER=postgres 2 | DB_PASS=password 3 | INSTANCE_HOST=localhost 4 | DB_PORT=5432 5 | DB_NAME=postgres 6 | INSTANCE_ID= -------------------------------------------------------------------------------- /rest-api-container/.gitignore: -------------------------------------------------------------------------------- 1 | keys.json 2 | .docker.env 3 | secret.yaml 4 | -------------------------------------------------------------------------------- /rest-api-container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine AS builder 2 | 3 | # Create the user and group files that will be used in the running container to 4 | # run the process as an unprivileged user. 5 | RUN mkdir /user && \ 6 | echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \ 7 | echo 'nobody:x:65534:' > /user/group 8 | 9 | # Install the Certificate-Authority certificates for the app to be able to make 10 | # calls to HTTPS endpoints. 11 | # Git is required for fetching the dependencies. 12 | RUN apk update \ 13 | && apk add --no-cache git \ 14 | && apk add ca-certificates 15 | 16 | # Set the working directory outside $GOPATH to enable the support for modules. 17 | WORKDIR /src 18 | 19 | # Fetch dependencies first; they are less susceptible to change on every build 20 | # and will therefore be cached for speeding up the next build 21 | COPY ./go.mod ./go.sum ./ 22 | RUN go mod download 23 | 24 | # Import the code from the context. 25 | COPY pkg/ pkg/ 26 | COPY cmd/ cmd/ 27 | COPY internal/ internal/ 28 | COPY database/ database/ 29 | 30 | # Build the executable to `/app`. Mark the build as statically linked. 31 | RUN CGO_ENABLED=0 go build \ 32 | -installsuffix 'static' \ 33 | -o /app ./cmd/web/main.go 34 | 35 | # Final stage: the running container. 36 | FROM scratch AS final 37 | 38 | # Import the user and group files from the first stage. 39 | COPY --from=builder /user/group /user/passwd /etc/ 40 | 41 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 42 | # Import the compiled executable from the first stage. 43 | COPY --from=builder /app /app 44 | 45 | # Declare the port on which the webserver will be exposed. 46 | # As we're going to run the executable as an unprivileged user, we can't bind 47 | # to ports below 1024. 48 | EXPOSE 7999 49 | 50 | # Perform any further action as an unprivileged user. 51 | USER nobody:nobody 52 | 53 | # Run the compiled binary. 54 | ENTRYPOINT ["/app"] 55 | -------------------------------------------------------------------------------- /rest-api-container/Readme.md: -------------------------------------------------------------------------------- 1 | # Application Delivery 2 | 3 | The best application is a running application. For our we worked on creating some amazing REST API so far but its no use to anyone if we don't have it accessible to others. 4 | 5 | ## Kubernetes 6 | 7 | We can run the docker-compose in a virtual machine and expose it onto the internet that way. But these days for many software teams Kubernetes is the defacto place to deploy containers. Kubernetes is an open-source tool for managing container workloads. It has some awesome properties like self healing and auto-scaling. Its also the center of many cloud native technologies that makes the application delivery experience even better. 8 | 9 | For this example we will deploy to a GKE Standard cluster. To enable our `cloud-sql-proxy` to connect to our database we will have to set up some IAM policies [as shown here](https://cloud.google.com/sql/docs/postgres/connect-kubernetes-engine#service-account-key) 10 | 11 | ```bash 12 | git clone origin/containers-02 13 | ``` 14 | 15 | If you are not already in the folder 16 | 17 | ```bash 18 | cd rest-api-container 19 | ``` 20 | 21 | ```bash 22 | kubectl apply -f kubernetes/ 23 | ``` 24 | 25 | After a while we should see that our application is deployed 26 | 27 | ```bash 28 | kubectl get deploy 29 | ``` 30 | 31 | ```bash 32 | NAME READY UP-TO-DATE AVAILABLE AGE 33 | restapi 1/1 1 1 64m 34 | ``` 35 | 36 | We can get the external IP from our svc 37 | 38 | ```bash 39 | kubectl get svc 40 | ``` 41 | 42 | ```bash 43 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 44 | kubernetes ClusterIP 10.52.0.1 443/TCP 5d8h 45 | restapi LoadBalancer 10.52.7.179 80:30981/TCP 69m 46 | ``` 47 | 48 | We can access our application at the IP 49 | 50 | ```bash 51 | curl -s http:///api/v1/users/1 | jq 52 | ``` 53 | 54 | We should get 55 | 56 | ```json 57 | { 58 | "id": 1, 59 | "name": "Travis Miller", 60 | "email": "michellebrooks@williams.net", 61 | "company": "Russell-Rowe", 62 | "interests": [ 63 | "Jenkins", 64 | "Jupyter Notebook", 65 | "Swift", 66 | "Scrum", 67 | "Spark", 68 | "Kubeflow", 69 | "GraphQL", 70 | "CSS", 71 | "Java", 72 | "Go", 73 | "Openshift", 74 | "C++", 75 | "Data Encryption" 76 | ] 77 | } 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /rest-api-container/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "cloud.google.com/go/cloudsqlconn" 10 | "cloud.google.com/go/cloudsqlconn/postgres/pgxv4" 11 | "github.com/labstack/echo/v4" 12 | echoMiddleware "github.com/labstack/echo/v4/middleware" 13 | "github.com/moficodes/restful-go-api/database/internal/datasource" 14 | "github.com/moficodes/restful-go-api/database/internal/handler" 15 | "github.com/moficodes/restful-go-api/database/pkg/middleware" 16 | 17 | _ "github.com/joho/godotenv/autoload" 18 | ) 19 | 20 | type server struct{} 21 | 22 | func Chain(h echo.HandlerFunc, middleware ...func(echo.HandlerFunc) echo.HandlerFunc) echo.HandlerFunc { 23 | for _, m := range middleware { 24 | h = m(h) 25 | } 26 | return h 27 | } 28 | 29 | // getDB creates a connection to the database 30 | // based on environment variables. 31 | func getDB() (*sql.DB, func() error) { 32 | cleanup, err := pgxv4.RegisterDriver("cloudsql-postgres", cloudsqlconn.WithIAMAuthN()) 33 | if err != nil { 34 | log.Fatalf("Error on pgxv4.RegisterDriver: %v", err) 35 | } 36 | 37 | dsn := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable", os.Getenv("INSTANCE_CONNECTION_NAME"), os.Getenv("DB_USER"), os.Getenv("DB_NAME")) 38 | db, err := sql.Open("cloudsql-postgres", dsn) 39 | if err != nil { 40 | log.Fatalf("Error on sql.Open: %v", err) 41 | } 42 | 43 | return db, cleanup 44 | } 45 | 46 | func main() { 47 | // var ( 48 | // dbUser = os.Getenv("DB_USER") // e.g. 'my-db-user' 49 | // dbPwd = os.Getenv("DB_PASS") // e.g. 'my-db-password' 50 | // dbTCPHost = os.Getenv("INSTANCE_HOST") // e.g. '127.0.0.1' ('172.17.0.1' if deployed to GAE Flex) 51 | // dbPort = os.Getenv("DB_PORT") // e.g. '5432' 52 | // dbName = os.Getenv("DB_NAME") // e.g. 'my-database' 53 | // ) 54 | 55 | // dbURI := fmt.Sprintf("host=%s user=%s password=%s port=%s database=%s", 56 | // dbTCPHost, dbUser, dbPwd, dbPort, dbName) 57 | // pool, err := database.PGPool(context.Background(), dbURI) 58 | // if err != nil { 59 | // log.Fatalln(err) 60 | // } 61 | // defer pool.Close() 62 | 63 | db, cleanup := getDB() 64 | defer cleanup() 65 | 66 | p := datasource.NewSQL(db) 67 | // p := datasource.NewPostgres(pool) 68 | h := handler.NewHandler(p) 69 | 70 | e := echo.New() 71 | 72 | specialLogger := echoMiddleware.LoggerWithConfig(echoMiddleware.LoggerConfig{ 73 | Format: "time=${time_rfc3339} method=${method}, uri=${uri}, status=${status}, latency=${latency_human}, \n", 74 | }) 75 | e.Use(middleware.Logger, specialLogger) 76 | 77 | auth := e.Group("/auth") 78 | auth.Use(middleware.JWT) 79 | auth.GET("/test", handler.Authenticated) 80 | api := e.Group("/api/v1") 81 | api.GET("/users", h.GetAllUsers) 82 | api.GET("/instructors", h.GetAllInstructors) 83 | api.GET("/courses", h.GetAllCourses) 84 | 85 | api.GET("/users/:id", h.GetUserByID) 86 | api.GET("/instructors/:id", h.GetInstructorByID) 87 | api.GET("/courses/:id", h.GetCoursesByID) 88 | 89 | api.GET("/courses/instructor/:instructorID", h.GetCoursesForInstructor) 90 | api.GET("/courses/user/:userID", h.GetCoursesForUser) 91 | 92 | api.POST("/users", h.CreateNewUser) 93 | api.POST("/users/:id/interests", h.AddUserInterest) 94 | 95 | port := "7999" 96 | 97 | e.Logger.Fatal(e.Start(":" + port)) 98 | } 99 | -------------------------------------------------------------------------------- /rest-api-container/database/migration/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | drop table if exists "UserInterest"; 2 | drop table if exists "InstructorExpertise"; 3 | drop table if exists "CourseTopic"; 4 | drop table if exists "CourseAttendee"; 5 | drop table if exists "Users"; 6 | drop table if exists "Courses"; 7 | drop table if exists "Instructors"; -------------------------------------------------------------------------------- /rest-api-container/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | cloudsql-proxy: 4 | container_name: cloudsql-proxy 5 | image: gcr.io/cloudsql-docker/gce-proxy:1.32.0 6 | command: /cloud_sql_proxy --dir=/cloudsql -instances=${INSTANCE_ID}=tcp:0.0.0.0:5432 -credential_file=/secrets/cloudsql/credentials.json 7 | ports: 8 | - 127.0.0.1:5432:5432 9 | volumes: 10 | - ./keys.json:/secrets/cloudsql/credentials.json 11 | restart: always 12 | restapi: 13 | container_name: restapi 14 | image: moficodes/restapi:1.0 15 | command: /app 16 | env_file: 17 | - .docker.env 18 | ports: 19 | - 7999:7999 20 | restart: always -------------------------------------------------------------------------------- /rest-api-container/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/database 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/doug-martin/goqu/v9 v9.18.0 8 | github.com/golang-migrate/migrate/v4 v4.14.1 9 | github.com/jackc/pgx/v4 v4.18.1 10 | github.com/joho/godotenv v1.5.1 11 | github.com/labstack/echo/v4 v4.10.0 12 | github.com/ory/dockertest/v3 v3.6.3 13 | github.com/stretchr/testify v1.8.1 14 | ) 15 | 16 | require ( 17 | cloud.google.com/go/cloudsqlconn v1.2.3 // indirect 18 | cloud.google.com/go/compute v1.19.0 // indirect 19 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 20 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 21 | github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect 22 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 23 | github.com/cenkalti/backoff/v3 v3.0.0 // indirect 24 | github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/docker/go-connections v0.4.0 // indirect 27 | github.com/docker/go-units v0.4.0 // indirect 28 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 29 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 30 | github.com/golang/protobuf v1.5.3 // indirect 31 | github.com/google/s2a-go v0.1.0 // indirect 32 | github.com/google/uuid v1.3.0 // indirect 33 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 34 | github.com/googleapis/gax-go/v2 v2.8.0 // indirect 35 | github.com/hashicorp/errwrap v1.0.0 // indirect 36 | github.com/hashicorp/go-multierror v1.1.0 // indirect 37 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 38 | github.com/jackc/pgconn v1.14.0 // indirect 39 | github.com/jackc/pgio v1.0.0 // indirect 40 | github.com/jackc/pgpassfile v1.0.0 // indirect 41 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect 42 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 43 | github.com/jackc/pgtype v1.14.0 // indirect 44 | github.com/jackc/puddle v1.3.0 // indirect 45 | github.com/labstack/gommon v0.4.0 // indirect 46 | github.com/lib/pq v1.10.2 // indirect 47 | github.com/mattn/go-colorable v0.1.13 // indirect 48 | github.com/mattn/go-isatty v0.0.17 // indirect 49 | github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 // indirect 50 | github.com/opencontainers/go-digest v1.0.0 // indirect 51 | github.com/opencontainers/image-spec v1.0.1 // indirect 52 | github.com/opencontainers/runc v1.0.0-rc9 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/pmezard/go-difflib v1.0.0 // indirect 55 | github.com/sirupsen/logrus v1.7.0 // indirect 56 | github.com/valyala/bytebufferpool v1.0.0 // indirect 57 | github.com/valyala/fasttemplate v1.2.2 // indirect 58 | go.opencensus.io v0.24.0 // indirect 59 | golang.org/x/crypto v0.6.0 // indirect 60 | golang.org/x/net v0.9.0 // indirect 61 | golang.org/x/oauth2 v0.7.0 // indirect 62 | golang.org/x/sys v0.7.0 // indirect 63 | golang.org/x/text v0.9.0 // indirect 64 | golang.org/x/time v0.3.0 // indirect 65 | google.golang.org/api v0.117.0 // indirect 66 | google.golang.org/appengine v1.6.7 // indirect 67 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 68 | google.golang.org/grpc v1.54.0 // indirect 69 | google.golang.org/protobuf v1.30.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /rest-api-container/internal/datasource/db.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | type DB interface { 4 | GetAllCourses() ([]Course, error) 5 | GetAllInstructors() ([]Instructor, error) 6 | GetAllUsers() ([]User, error) 7 | GetCoursesByID(int) (*Course, error) 8 | GetInstructorByID(int) (*Instructor, error) 9 | GetUserByID(int) (*User, error) 10 | GetCoursesForInstructor(int) ([]Course, error) 11 | GetCoursesForUser(int) ([]Course, error) 12 | 13 | CreateNewUser(*User) (int, error) 14 | AddUserInterest(int, []string) (int, error) 15 | } 16 | -------------------------------------------------------------------------------- /rest-api-container/internal/datasource/postgres_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/golang-migrate/migrate/v4" 11 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 12 | _ "github.com/golang-migrate/migrate/v4/source/file" 13 | "github.com/jackc/pgx/v4/pgxpool" 14 | "github.com/ory/dockertest/v3" 15 | "github.com/ory/dockertest/v3/docker" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | var pgpool *pgxpool.Pool 20 | 21 | func TestMain(m *testing.M) { 22 | // uses a sensible default on windows (tcp/http) and linux/osx (socket) 23 | pool, err := dockertest.NewPool("") 24 | if err != nil { 25 | log.Fatalf("Could not connect to docker: %s", err) 26 | } 27 | 28 | // pulls an image, creates a container based on it and runs it 29 | resource, err := pool.RunWithOptions(&dockertest.RunOptions{ 30 | Repository: "postgres", 31 | Tag: "9.6", 32 | Env: []string{ 33 | "POSTGRES_DB=postgres", 34 | "POSTGRES_PASSWORD=password", 35 | }, 36 | }, func(config *docker.HostConfig) { 37 | // set AutoRemove to true so that stopped container goes away by itself 38 | config.AutoRemove = true 39 | config.RestartPolicy = docker.RestartPolicy{ 40 | Name: "no", 41 | } 42 | }) 43 | if err != nil { 44 | log.Fatalf("Could not start resource: %s", err) 45 | } 46 | 47 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 48 | if err := pool.Retry(func() error { 49 | var err error 50 | pgpool, err = pgxpool.Connect(context.Background(), fmt.Sprintf("postgresql://postgres:password@localhost:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp"))) 51 | if err != nil { 52 | return err 53 | } 54 | return pgpool.Ping(context.Background()) 55 | }); err != nil { 56 | log.Fatalf("Could not connect to docker: %s", err) 57 | } 58 | 59 | mig, err := migrate.New("file://../../database/migration", fmt.Sprintf("postgresql://postgres:password@localhost:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp"))) 60 | if err != nil { 61 | log.Fatalln(err) 62 | } 63 | 64 | if err := mig.Up(); err != nil { 65 | log.Fatalln(err) 66 | } 67 | 68 | code := m.Run() 69 | 70 | // You can't defer this because os.Exit doesn't care for defer 71 | if err := pool.Purge(resource); err != nil { 72 | log.Fatalf("Could not purge resource: %s", err) 73 | } 74 | 75 | os.Exit(code) 76 | } 77 | 78 | func TestPostgres_GetAllUsers(t *testing.T) { 79 | p := NewPostgres(pgpool) 80 | users, err := p.GetAllUsers() 81 | if err != nil { 82 | t.Errorf("getalluser err=%s; want nil", err) 83 | } 84 | // not great for parallel tests. 85 | want := 500 86 | got := len(users) 87 | if got != want { 88 | t.Errorf("want: %d, got: %d", want, got) 89 | } 90 | } 91 | func TestPostgres_GetUserByID_success(t *testing.T) { 92 | p := NewPostgres(pgpool) 93 | user, err := p.GetUserByID(1) 94 | if err != nil { 95 | t.Errorf("getuserbyid(1) err=%s; want nil", err) 96 | } 97 | want := 1 98 | got := user.ID 99 | if got != want { 100 | t.Errorf("want: %d, got: %d", want, got) 101 | } 102 | } 103 | 104 | func TestPostgres_GetUserByID_failure(t *testing.T) { 105 | p := NewPostgres(pgpool) 106 | user, err := p.GetUserByID(-1) // this should return nil user 107 | assert.Error(t, err, "should return error") 108 | assert.Nil(t, user, "user should be nil") 109 | } 110 | -------------------------------------------------------------------------------- /rest-api-container/internal/datasource/type.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type intarr []int 12 | type stringarr []string 13 | 14 | // custom Scan function for scanning intarr values from database 15 | func (i *intarr) Scan(value interface{}) error { 16 | source, ok := value.(string) 17 | if !ok { 18 | return errors.New("incompatible type") 19 | } 20 | log.Println(source) 21 | source = strings.Trim(source, "{}\\\"") 22 | var res intarr 23 | ints := strings.Split(source, ",") 24 | for _, i := range ints { 25 | num, err := strconv.Atoi(i) 26 | if err != nil { 27 | return fmt.Errorf("can not convert %v to int : %v", i, err) 28 | } 29 | res = append(res, num) 30 | } 31 | *i = res 32 | return nil 33 | } 34 | 35 | func (i *stringarr) Scan(value interface{}) error { 36 | source, ok := value.(string) 37 | if !ok { 38 | return errors.New("incompatible type") 39 | } 40 | source = strings.Trim(source, "{}\\\"") 41 | log.Println(source) 42 | var res stringarr 43 | data := strings.Split(source, ",") 44 | for _, val := range data { 45 | val = strings.Trim(val, "\"") 46 | res = append(res, val) 47 | } 48 | *i = res 49 | return nil 50 | } 51 | 52 | type User struct { 53 | ID int `json:"id" db:"id"` 54 | Name string `json:"name" db:"name"` 55 | Email string `json:"email" db:"email"` 56 | Company string `json:"company" db:"company"` 57 | Interests stringarr `json:"interests,omitempty" db:"interests"` 58 | } 59 | 60 | // Instructor type represent a instructor for a course 61 | type Instructor struct { 62 | ID int `json:"id" db:"id"` 63 | Name string `json:"name" db:"name"` 64 | Email string `json:"email" db:"email"` 65 | Company string `json:"company" db:"company"` 66 | Expertise stringarr `json:"expertise,omitempty" db:"expertise"` 67 | } 68 | 69 | // Course is course being taught 70 | type Course struct { 71 | ID int `json:"id" db:"id"` 72 | InstructorID int `json:"instructor_id" db:"instructorId"` 73 | Name string `json:"name" db:"name"` 74 | Topics stringarr `json:"topics,omitempty" db:"topics"` 75 | Attendees intarr `json:"attendees,omitempty" db:"attendees"` 76 | } 77 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/database/pkg/middleware" 9 | ) 10 | 11 | func Authenticated(c echo.Context) error { 12 | cc := c.(*middleware.CustomContext) 13 | _name, ok := cc.Claims["name"] 14 | if !ok { 15 | echo.NewHTTPError(http.StatusUnauthorized, "malformed jwt") 16 | } 17 | 18 | name := fmt.Sprintf("%v", _name) 19 | 20 | return c.JSON(http.StatusOK, Message{Data: name}) 21 | } 22 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/courses.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func (h *Handler) GetAllCourses(c echo.Context) error { 12 | courses, err := h.DB.GetAllCourses() 13 | if err != nil { 14 | log.Println(err) 15 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 16 | } 17 | return c.JSON(http.StatusOK, courses) 18 | } 19 | 20 | func (h *Handler) GetCoursesByID(c echo.Context) error { 21 | id := -1 22 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 23 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 24 | } 25 | 26 | course, err := h.DB.GetCoursesByID(id) 27 | 28 | if err != nil { 29 | log.Println(err) 30 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 31 | } 32 | 33 | if course == nil { 34 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("course with id : %d not found", id)) 35 | } 36 | 37 | return c.JSON(http.StatusOK, course) 38 | } 39 | 40 | func (h *Handler) GetCoursesForInstructor(c echo.Context) error { 41 | id := -1 42 | if err := echo.PathParamsBinder(c).Int("instructorID", &id).BindError(); err != nil { 43 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 44 | } 45 | courses, err := h.DB.GetCoursesForInstructor(id) 46 | if err != nil { 47 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 48 | } 49 | return c.JSON(http.StatusOK, courses) 50 | } 51 | 52 | func (h *Handler) GetCoursesForUser(c echo.Context) error { 53 | id := -1 54 | if err := echo.PathParamsBinder(c).Int("userID", &id).BindError(); err != nil { 55 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 56 | } 57 | courses, err := h.DB.GetCoursesForUser(id) 58 | if err != nil { 59 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 60 | } 61 | return c.JSON(http.StatusOK, courses) 62 | } 63 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/courses_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/moficodes/restful-go-api/database/internal/datasource" 14 | ) 15 | 16 | type mock struct { 17 | users []datasource.User 18 | courses []datasource.Course 19 | instructors []datasource.Instructor 20 | } 21 | 22 | var ( 23 | course = datasource.Course{ 24 | ID: 1, 25 | InstructorID: 1, 26 | Name: "Test Course", 27 | Topics: []string{"go"}, 28 | Attendees: []int{1}, 29 | } 30 | ) 31 | 32 | func TestHandler_GetAllCourses(t *testing.T) { 33 | m := &mock{courses: []datasource.Course{course}} 34 | h := NewHandler(m) 35 | e := echo.New() 36 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses", nil) 37 | w := httptest.NewRecorder() 38 | c := e.NewContext(r, w) 39 | 40 | if assert.NoError(t, h.GetAllCourses(c)) { 41 | assert.Equal(t, http.StatusOK, w.Code) 42 | var courses []datasource.Course 43 | assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &courses)) 44 | assert.Equal(t, course, courses[0]) 45 | } 46 | } 47 | 48 | func TestHandler_GetCoursesByID_success(t *testing.T) { 49 | m := &mock{courses: []datasource.Course{course}} 50 | h := NewHandler(m) 51 | e := echo.New() 52 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses/1", nil) 53 | w := httptest.NewRecorder() 54 | c := e.NewContext(r, w) 55 | c.SetPath("/api/v1/courses/:id") 56 | c.SetParamNames("id") 57 | c.SetParamValues("1") 58 | 59 | if assert.NoError(t, h.GetCoursesByID(c)) { 60 | assert.Equal(t, http.StatusOK, w.Code) 61 | var course *datasource.Course 62 | assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &course)) 63 | assert.Equal(t, 1, course.ID) 64 | } 65 | } 66 | 67 | func TestHandler_GetCoursesByID_failure(t *testing.T) { 68 | m := &mock{courses: []datasource.Course{course}} 69 | h := NewHandler(m) 70 | e := echo.New() 71 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses/5", nil) 72 | w := httptest.NewRecorder() 73 | c := e.NewContext(r, w) 74 | c.SetPath("/api/v1/courses/:id") 75 | c.SetParamNames("id") 76 | c.SetParamValues("5") 77 | 78 | assert.Error(t, h.GetUserByID(c), "should return error") 79 | } 80 | 81 | func (m *mock) GetAllCourses() ([]datasource.Course, error) { 82 | return m.courses, nil 83 | } 84 | 85 | func (m *mock) GetAllUsers() ([]datasource.User, error) { 86 | return nil, nil 87 | } 88 | 89 | func (m *mock) GetAllInstructors() ([]datasource.Instructor, error) { 90 | return nil, nil 91 | } 92 | 93 | func (m *mock) GetCoursesByID(id int) (*datasource.Course, error) { 94 | for _, course := range m.courses { 95 | if course.ID == id { 96 | return &course, nil 97 | } 98 | } 99 | return nil, errors.New("bad stuff happened") 100 | } 101 | func (m *mock) GetInstructorByID(id int) (*datasource.Instructor, error) { 102 | return nil, nil 103 | } 104 | func (m *mock) GetUserByID(id int) (*datasource.User, error) { 105 | return nil, nil 106 | } 107 | 108 | func (m *mock) GetCoursesForInstructor(id int) ([]datasource.Course, error) { 109 | return nil, nil 110 | } 111 | func (m *mock) GetCoursesForUser(id int) ([]datasource.Course, error) { 112 | return nil, nil 113 | } 114 | 115 | func (m *mock) CreateNewUser(*datasource.User) (int, error) { 116 | return -1, nil 117 | } 118 | func (m *mock) AddUserInterest(id int, interests []string) (int, error) { 119 | return -1, nil 120 | } 121 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/moficodes/restful-go-api/database/internal/datasource" 5 | ) 6 | 7 | func NewHandler(db datasource.DB) *Handler { 8 | h := Handler{db} 9 | return &h 10 | } 11 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/instructors.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func (h *Handler) GetAllInstructors(c echo.Context) error { 12 | expertise := []string{} 13 | 14 | // the key was found. 15 | if err := echo.QueryParamsBinder(c).Strings("expertise", &expertise).BindError(); err != nil { //watch the == here 16 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 17 | } 18 | 19 | instructors, err := h.DB.GetAllInstructors() 20 | if err != nil { 21 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 22 | } 23 | 24 | return c.JSON(http.StatusOK, instructors) 25 | } 26 | 27 | func (h *Handler) GetInstructorByID(c echo.Context) error { 28 | id := -1 29 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 30 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 31 | } 32 | 33 | instructor, err := h.DB.GetInstructorByID(id) 34 | if err != nil { 35 | log.Println(err) 36 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 37 | } 38 | 39 | if instructor == nil { 40 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("instructor with id : %d not found", id)) 41 | } 42 | 43 | return c.JSON(http.StatusOK, instructor) 44 | } 45 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/type.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/moficodes/restful-go-api/database/internal/datasource" 5 | ) 6 | 7 | type Handler struct { 8 | datasource.DB 9 | } 10 | 11 | type Message struct { 12 | Data string `json:"data"` 13 | } 14 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/users.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/database/internal/datasource" 9 | ) 10 | 11 | func (h *Handler) GetAllUsers(c echo.Context) error { 12 | users, err := h.DB.GetAllUsers() 13 | 14 | if err != nil { 15 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 16 | } 17 | 18 | return c.JSON(http.StatusOK, users) 19 | } 20 | 21 | func (h *Handler) GetUserByID(c echo.Context) error { 22 | id := -1 23 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 24 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 25 | } 26 | 27 | user, err := h.DB.GetUserByID(id) 28 | if err != nil { 29 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 30 | } 31 | 32 | if user == nil { 33 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("user with id : %d not found", id)) 34 | } 35 | 36 | return c.JSON(http.StatusOK, user) 37 | } 38 | 39 | func (h *Handler) CreateNewUser(c echo.Context) error { 40 | c.Request().Header.Add("Content-Type", "application/json") 41 | if c.Request().ContentLength == 0 { 42 | return echo.NewHTTPError(http.StatusBadRequest, "body is required for this method") 43 | } 44 | user := new(datasource.User) 45 | err := c.Bind(user) 46 | if err != nil { 47 | return echo.NewHTTPError(http.StatusBadRequest, "body could not be parsed") 48 | } 49 | 50 | id, err := h.DB.CreateNewUser(user) 51 | if err != nil { 52 | return echo.NewHTTPError(http.StatusBadRequest, "could not create user") 53 | } 54 | 55 | return c.JSON(http.StatusCreated, Message{Data: fmt.Sprintf("user created with id : %d", id)}) 56 | } 57 | 58 | func (h *Handler) AddUserInterest(c echo.Context) error { 59 | c.Request().Header.Add("Content-Type", "application/json") 60 | 61 | id := -1 62 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 63 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 64 | } 65 | 66 | if c.Request().ContentLength == 0 { 67 | return echo.NewHTTPError(http.StatusBadRequest, "body is required for this method") 68 | } 69 | 70 | var interests []string 71 | err := c.Bind(&interests) 72 | if err != nil { 73 | return echo.NewHTTPError(http.StatusBadRequest, "body not be valid") 74 | } 75 | count, err := h.DB.AddUserInterest(id, interests) 76 | if err != nil { 77 | return echo.NewHTTPError(http.StatusBadRequest, "could not add user interest") 78 | } 79 | return c.JSON(http.StatusCreated, Message{Data: fmt.Sprintf("%d user interest added", count)}) 80 | } 81 | -------------------------------------------------------------------------------- /rest-api-container/internal/handler/util.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /rest-api-container/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: restapi 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: restapi 9 | template: 10 | metadata: 11 | labels: 12 | app: restapi 13 | spec: 14 | securityContext: 15 | runAsUser: 65534 16 | runAsGroup: 65534 17 | serviceAccountName: developer 18 | containers: 19 | - name: restapi 20 | image: moficodes/restapi:v1.7.7 21 | ports: 22 | - containerPort: 7999 23 | resources: 24 | requests: 25 | cpu: 500m 26 | limits: 27 | cpu: 500m 28 | env: 29 | - name: INSTANCE_CONNECTION_NAME 30 | valueFrom: 31 | secretKeyRef: 32 | name: connection-config 33 | key: instance-connection-name 34 | - name: DB_USER 35 | valueFrom: 36 | secretKeyRef: 37 | name: connection-config 38 | key: username 39 | - name: DB_NAME 40 | valueFrom: 41 | secretKeyRef: 42 | name: connection-config 43 | key: database 44 | -------------------------------------------------------------------------------- /rest-api-container/kubernetes/secret.example.yaml: -------------------------------------------------------------------------------- 1 | kind: Secret 2 | apiVersion: v1 3 | data: 4 | database: BASE64_ENCODED_DB_NAME 5 | username: BASE64_ENCODED_DB_USER 6 | instance-connection-name: BASE64_ENCODED_INSTANCE_CONNECTION_NAME 7 | metadata: 8 | name: connection-config -------------------------------------------------------------------------------- /rest-api-container/kubernetes/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: developer -------------------------------------------------------------------------------- /rest-api-container/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: restapi 5 | spec: 6 | type: LoadBalancer 7 | selector: 8 | app: restapi 9 | ports: 10 | - port: 80 11 | targetPort: 7999 -------------------------------------------------------------------------------- /rest-api-container/pkg/database/postgres.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v4/pgxpool" 7 | ) 8 | 9 | func PGPool(ctx context.Context, connStr string) (*pgxpool.Pool, error) { 10 | pool, err := pgxpool.Connect(ctx, connStr) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return pool, nil 15 | } 16 | -------------------------------------------------------------------------------- /rest-api-container/pkg/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | type CustomContext struct { 13 | echo.Context 14 | Claims jwt.MapClaims 15 | } 16 | 17 | func JWT(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | authorization := c.Request().Header.Get("Authorization") 20 | // auth token have the structure `bearer ` 21 | // so we split it on the ` ` (space character) 22 | splitToken := strings.Split(authorization, " ") 23 | // if we end up with a array of size 2 we have the token as the 24 | // 2nd item in the array 25 | if len(splitToken) != 2 { 26 | // we got something different 27 | return echo.NewHTTPError(http.StatusUnauthorized, "no valid token found") 28 | } 29 | // second item is our possible token 30 | jwtToken := splitToken[1] 31 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 32 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 33 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 34 | } 35 | return []byte("very-secret"), nil 36 | }) 37 | 38 | if err != nil { 39 | // we got something different 40 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 41 | } 42 | 43 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 44 | cc := &CustomContext{c, claims} 45 | return next(cc) 46 | 47 | } else { 48 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rest-api-container/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Logger(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | log.Println(c.Request().URL) 12 | return next(c) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rest-api-database/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_CONNECTION_URL="postgresql://postgres:password@localhost:5432/postgres" -------------------------------------------------------------------------------- /rest-api-database/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | 8 | "github.com/labstack/echo/v4" 9 | echoMiddleware "github.com/labstack/echo/v4/middleware" 10 | "github.com/moficodes/restful-go-api/database/internal/datasource" 11 | "github.com/moficodes/restful-go-api/database/internal/handler" 12 | "github.com/moficodes/restful-go-api/database/pkg/database" 13 | "github.com/moficodes/restful-go-api/database/pkg/middleware" 14 | 15 | _ "github.com/joho/godotenv/autoload" 16 | ) 17 | 18 | type server struct{} 19 | 20 | func Chain(h echo.HandlerFunc, middleware ...func(echo.HandlerFunc) echo.HandlerFunc) echo.HandlerFunc { 21 | for _, m := range middleware { 22 | h = m(h) 23 | } 24 | return h 25 | } 26 | 27 | func main() { 28 | connStr := os.Getenv("DATABASE_CONNECTION_URL") 29 | pool, err := database.PGPool(context.Background(), connStr) 30 | if err != nil { 31 | log.Fatalln(err) 32 | } 33 | defer pool.Close() 34 | 35 | p := datasource.NewPostgres(pool) 36 | h := handler.NewHandler(p) 37 | 38 | e := echo.New() 39 | 40 | specialLogger := echoMiddleware.LoggerWithConfig(echoMiddleware.LoggerConfig{ 41 | Format: "time=${time_rfc3339} method=${method}, uri=${uri}, status=${status}, latency=${latency_human}, \n", 42 | }) 43 | e.Use(middleware.Logger, specialLogger) 44 | 45 | auth := e.Group("/auth") 46 | auth.Use(middleware.JWT) 47 | auth.GET("/test", handler.Authenticated) 48 | api := e.Group("/api/v1") 49 | api.GET("/users", h.GetAllUsers) 50 | api.GET("/instructors", h.GetAllInstructors) 51 | api.GET("/courses", h.GetAllCourses) 52 | 53 | api.GET("/users/:id", h.GetUserByID) 54 | api.GET("/instructors/:id", h.GetInstructorByID) 55 | api.GET("/courses/:id", h.GetCoursesByID) 56 | 57 | api.GET("/courses/instructor/:instructorID", h.GetCoursesForInstructor) 58 | api.GET("/courses/user/:userID", h.GetCoursesForUser) 59 | 60 | api.POST("/users", h.CreateNewUser) 61 | api.POST("/users/:id/interests", h.AddUserInterest) 62 | 63 | port := "7999" 64 | 65 | e.Logger.Fatal(e.Start(":" + port)) 66 | } 67 | -------------------------------------------------------------------------------- /rest-api-database/database/migration/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | drop table if exists "UserInterest"; 2 | drop table if exists "InstructorExpertise"; 3 | drop table if exists "CourseTopic"; 4 | drop table if exists "CourseAttendee"; 5 | drop table if exists "Users"; 6 | drop table if exists "Courses"; 7 | drop table if exists "Instructors"; -------------------------------------------------------------------------------- /rest-api-database/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/database 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/doug-martin/goqu/v9 v9.18.0 8 | github.com/golang-migrate/migrate/v4 v4.15.2 9 | github.com/jackc/pgx/v4 v4.18.0 10 | github.com/joho/godotenv v1.5.1 11 | github.com/labstack/echo/v4 v4.10.0 12 | github.com/ory/dockertest/v3 v3.9.1 13 | github.com/stretchr/testify v1.8.1 14 | ) 15 | 16 | require ( 17 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 18 | github.com/Microsoft/go-winio v0.6.0 // indirect 19 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 20 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 21 | github.com/cenkalti/backoff/v4 v4.2.0 // indirect 22 | github.com/containerd/continuity v0.3.0 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/docker/cli v23.0.1+incompatible // indirect 25 | github.com/docker/docker v23.0.1+incompatible // indirect 26 | github.com/docker/go-connections v0.4.0 // indirect 27 | github.com/docker/go-units v0.5.0 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 30 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 31 | github.com/hashicorp/errwrap v1.1.0 // indirect 32 | github.com/hashicorp/go-multierror v1.1.1 // indirect 33 | github.com/imdario/mergo v0.3.13 // indirect 34 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 35 | github.com/jackc/pgconn v1.14.0 // indirect 36 | github.com/jackc/pgio v1.0.0 // indirect 37 | github.com/jackc/pgpassfile v1.0.0 // indirect 38 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect 39 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 40 | github.com/jackc/pgtype v1.14.0 // indirect 41 | github.com/jackc/puddle v1.3.0 // indirect 42 | github.com/labstack/gommon v0.4.0 // indirect 43 | github.com/lib/pq v1.10.7 // indirect 44 | github.com/mattn/go-colorable v0.1.13 // indirect 45 | github.com/mattn/go-isatty v0.0.17 // indirect 46 | github.com/mitchellh/mapstructure v1.5.0 // indirect 47 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect 48 | github.com/opencontainers/go-digest v1.0.0 // indirect 49 | github.com/opencontainers/image-spec v1.0.2 // indirect 50 | github.com/opencontainers/runc v1.1.4 // indirect 51 | github.com/pkg/errors v0.9.1 // indirect 52 | github.com/pmezard/go-difflib v1.0.0 // indirect 53 | github.com/sirupsen/logrus v1.9.0 // indirect 54 | github.com/valyala/bytebufferpool v1.0.0 // indirect 55 | github.com/valyala/fasttemplate v1.2.2 // indirect 56 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 57 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 58 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 59 | go.uber.org/atomic v1.10.0 // indirect 60 | golang.org/x/crypto v0.6.0 // indirect 61 | golang.org/x/mod v0.8.0 // indirect 62 | golang.org/x/net v0.6.0 // indirect 63 | golang.org/x/sys v0.5.0 // indirect 64 | golang.org/x/text v0.7.0 // indirect 65 | golang.org/x/time v0.3.0 // indirect 66 | golang.org/x/tools v0.6.0 // indirect 67 | gopkg.in/yaml.v2 v2.4.0 // indirect 68 | gopkg.in/yaml.v3 v3.0.1 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /rest-api-database/internal/datasource/db.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | type DB interface { 4 | GetAllCourses() ([]Course, error) 5 | GetAllInstructors() ([]Instructor, error) 6 | GetAllUsers() ([]User, error) 7 | GetCoursesByID(int) (*Course, error) 8 | GetInstructorByID(int) (*Instructor, error) 9 | GetUserByID(int) (*User, error) 10 | GetCoursesForInstructor(int) ([]Course, error) 11 | GetCoursesForUser(int) ([]Course, error) 12 | 13 | CreateNewUser(*User) (int, error) 14 | AddUserInterest(int, []string) (int, error) 15 | } 16 | -------------------------------------------------------------------------------- /rest-api-database/internal/datasource/postgres_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/golang-migrate/migrate/v4" 11 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 12 | _ "github.com/golang-migrate/migrate/v4/source/file" 13 | "github.com/jackc/pgx/v4/pgxpool" 14 | "github.com/ory/dockertest/v3" 15 | "github.com/ory/dockertest/v3/docker" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | var pgpool *pgxpool.Pool 20 | 21 | func TestMain(m *testing.M) { 22 | // uses a sensible default on windows (tcp/http) and linux/osx (socket) 23 | pool, err := dockertest.NewPool("") 24 | if err != nil { 25 | log.Fatalf("Could not connect to docker: %s", err) 26 | } 27 | 28 | // pulls an image, creates a container based on it and runs it 29 | resource, err := pool.RunWithOptions(&dockertest.RunOptions{ 30 | Repository: "postgres", 31 | Tag: "9.6", 32 | Env: []string{ 33 | "POSTGRES_DB=postgres", 34 | "POSTGRES_PASSWORD=password", 35 | }, 36 | }, func(config *docker.HostConfig) { 37 | // set AutoRemove to true so that stopped container goes away by itself 38 | config.AutoRemove = true 39 | config.RestartPolicy = docker.RestartPolicy{ 40 | Name: "no", 41 | } 42 | }) 43 | if err != nil { 44 | log.Fatalf("Could not start resource: %s", err) 45 | } 46 | 47 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 48 | if err := pool.Retry(func() error { 49 | var err error 50 | pgpool, err = pgxpool.Connect(context.Background(), fmt.Sprintf("postgresql://postgres:password@localhost:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp"))) 51 | if err != nil { 52 | return err 53 | } 54 | return pgpool.Ping(context.Background()) 55 | }); err != nil { 56 | log.Fatalf("Could not connect to docker: %s", err) 57 | } 58 | 59 | mig, err := migrate.New("file://../../database/migration", fmt.Sprintf("postgresql://postgres:password@localhost:%s/postgres?sslmode=disable", resource.GetPort("5432/tcp"))) 60 | if err != nil { 61 | log.Fatalln(err) 62 | } 63 | 64 | if err := mig.Up(); err != nil { 65 | log.Fatalln(err) 66 | } 67 | 68 | code := m.Run() 69 | 70 | // You can't defer this because os.Exit doesn't care for defer 71 | if err := pool.Purge(resource); err != nil { 72 | log.Fatalf("Could not purge resource: %s", err) 73 | } 74 | 75 | os.Exit(code) 76 | } 77 | 78 | func TestPostgres_GetAllUsers(t *testing.T) { 79 | p := NewPostgres(pgpool) 80 | users, err := p.GetAllUsers() 81 | if err != nil { 82 | t.Errorf("getalluser err=%s; want nil", err) 83 | } 84 | // not great for parallel tests. 85 | want := 500 86 | got := len(users) 87 | if got != want { 88 | t.Errorf("want: %d, got: %d", want, got) 89 | } 90 | } 91 | func TestPostgres_GetUserByID_success(t *testing.T) { 92 | p := NewPostgres(pgpool) 93 | user, err := p.GetUserByID(1) 94 | if err != nil { 95 | t.Errorf("getuserbyid(1) err=%s; want nil", err) 96 | } 97 | want := 1 98 | got := user.ID 99 | if got != want { 100 | t.Errorf("want: %d, got: %d", want, got) 101 | } 102 | } 103 | 104 | func TestPostgres_GetUserByID_failure(t *testing.T) { 105 | p := NewPostgres(pgpool) 106 | user, err := p.GetUserByID(-1) // this should return nil user 107 | assert.Error(t, err, "should return error") 108 | assert.Nil(t, user, "user should be nil") 109 | } 110 | -------------------------------------------------------------------------------- /rest-api-database/internal/datasource/type.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | type User struct { 4 | ID int `json:"id" db:"id"` 5 | Name string `json:"name" db:"name"` 6 | Email string `json:"email" db:"email"` 7 | Company string `json:"company" db:"company"` 8 | Interests []string `json:"interests,omitempty" db:"interests"` 9 | } 10 | 11 | // Instructor type represent a instructor for a course 12 | type Instructor struct { 13 | ID int `json:"id" db:"id"` 14 | Name string `json:"name" db:"name"` 15 | Email string `json:"email" db:"email"` 16 | Company string `json:"company" db:"company"` 17 | Expertise []string `json:"expertise,omitempty" db:"expertise"` 18 | } 19 | 20 | // Course is course being taught 21 | type Course struct { 22 | ID int `json:"id" db:"id"` 23 | InstructorID int `json:"instructor_id" db:"instructorId"` 24 | Name string `json:"name" db:"name"` 25 | Topics []string `json:"topics,omitempty" db:"topics"` 26 | Attendees []int `json:"attendees,omitempty" db:"attendees"` 27 | } 28 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/database/pkg/middleware" 9 | ) 10 | 11 | func Authenticated(c echo.Context) error { 12 | cc := c.(*middleware.CustomContext) 13 | _name, ok := cc.Claims["name"] 14 | if !ok { 15 | echo.NewHTTPError(http.StatusUnauthorized, "malformed jwt") 16 | } 17 | 18 | name := fmt.Sprintf("%v", _name) 19 | 20 | return c.JSON(http.StatusOK, Message{Data: name}) 21 | } 22 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/courses.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func (h *Handler) GetAllCourses(c echo.Context) error { 11 | courses, err := h.DB.GetAllCourses() 12 | if err != nil { 13 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 14 | } 15 | return c.JSON(http.StatusOK, courses) 16 | } 17 | 18 | func (h *Handler) GetCoursesByID(c echo.Context) error { 19 | id := -1 20 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 21 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 22 | } 23 | 24 | course, err := h.DB.GetCoursesByID(id) 25 | 26 | if err != nil { 27 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 28 | } 29 | 30 | if course == nil { 31 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("course with id : %d not found", id)) 32 | } 33 | 34 | return c.JSON(http.StatusOK, course) 35 | } 36 | 37 | func (h *Handler) GetCoursesForInstructor(c echo.Context) error { 38 | id := -1 39 | if err := echo.PathParamsBinder(c).Int("instructorID", &id).BindError(); err != nil { 40 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 41 | } 42 | courses, err := h.DB.GetCoursesForInstructor(id) 43 | if err != nil { 44 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 45 | } 46 | return c.JSON(http.StatusOK, courses) 47 | } 48 | 49 | func (h *Handler) GetCoursesForUser(c echo.Context) error { 50 | id := -1 51 | if err := echo.PathParamsBinder(c).Int("userID", &id).BindError(); err != nil { 52 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 53 | } 54 | courses, err := h.DB.GetCoursesForUser(id) 55 | if err != nil { 56 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 57 | } 58 | return c.JSON(http.StatusOK, courses) 59 | } 60 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/courses_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/moficodes/restful-go-api/database/internal/datasource" 14 | ) 15 | 16 | type mock struct { 17 | users []datasource.User 18 | courses []datasource.Course 19 | instructors []datasource.Instructor 20 | } 21 | 22 | var ( 23 | course = datasource.Course{ 24 | ID: 1, 25 | InstructorID: 1, 26 | Name: "Test Course", 27 | Topics: []string{"go"}, 28 | Attendees: []int{1}, 29 | } 30 | ) 31 | 32 | func TestHandler_GetAllCourses(t *testing.T) { 33 | m := &mock{courses: []datasource.Course{course}} 34 | h := NewHandler(m) 35 | e := echo.New() 36 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses", nil) 37 | w := httptest.NewRecorder() 38 | c := e.NewContext(r, w) 39 | 40 | if assert.NoError(t, h.GetAllCourses(c)) { 41 | assert.Equal(t, http.StatusOK, w.Code) 42 | var courses []datasource.Course 43 | assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &courses)) 44 | assert.Equal(t, course, courses[0]) 45 | } 46 | } 47 | 48 | func TestHandler_GetCoursesByID_success(t *testing.T) { 49 | m := &mock{courses: []datasource.Course{course}} 50 | h := NewHandler(m) 51 | e := echo.New() 52 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses/1", nil) 53 | w := httptest.NewRecorder() 54 | c := e.NewContext(r, w) 55 | c.SetPath("/api/v1/courses/:id") 56 | c.SetParamNames("id") 57 | c.SetParamValues("1") 58 | 59 | if assert.NoError(t, h.GetCoursesByID(c)) { 60 | assert.Equal(t, http.StatusOK, w.Code) 61 | var course *datasource.Course 62 | assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &course)) 63 | assert.Equal(t, 1, course.ID) 64 | } 65 | } 66 | 67 | func TestHandler_GetCoursesByID_failure(t *testing.T) { 68 | m := &mock{courses: []datasource.Course{course}} 69 | h := NewHandler(m) 70 | e := echo.New() 71 | r := httptest.NewRequest(http.MethodGet, "/api/v1/courses/5", nil) 72 | w := httptest.NewRecorder() 73 | c := e.NewContext(r, w) 74 | c.SetPath("/api/v1/courses/:id") 75 | c.SetParamNames("id") 76 | c.SetParamValues("5") 77 | 78 | assert.Error(t, h.GetUserByID(c), "should return error") 79 | } 80 | 81 | func (m *mock) GetAllCourses() ([]datasource.Course, error) { 82 | return m.courses, nil 83 | } 84 | 85 | func (m *mock) GetAllUsers() ([]datasource.User, error) { 86 | return nil, nil 87 | } 88 | 89 | func (m *mock) GetAllInstructors() ([]datasource.Instructor, error) { 90 | return nil, nil 91 | } 92 | 93 | func (m *mock) GetCoursesByID(id int) (*datasource.Course, error) { 94 | for _, course := range m.courses { 95 | if course.ID == id { 96 | return &course, nil 97 | } 98 | } 99 | return nil, errors.New("bad stuff happened") 100 | } 101 | func (m *mock) GetInstructorByID(id int) (*datasource.Instructor, error) { 102 | return nil, nil 103 | } 104 | func (m *mock) GetUserByID(id int) (*datasource.User, error) { 105 | return nil, nil 106 | } 107 | 108 | func (m *mock) GetCoursesForInstructor(id int) ([]datasource.Course, error) { 109 | return nil, nil 110 | } 111 | func (m *mock) GetCoursesForUser(id int) ([]datasource.Course, error) { 112 | return nil, nil 113 | } 114 | 115 | func (m *mock) CreateNewUser(*datasource.User) (int, error) { 116 | return -1, nil 117 | } 118 | func (m *mock) AddUserInterest(id int, interests []string) (int, error) { 119 | return -1, nil 120 | } 121 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/moficodes/restful-go-api/database/internal/datasource" 5 | ) 6 | 7 | func NewHandler(db datasource.DB) *Handler { 8 | h := Handler{db} 9 | return &h 10 | } 11 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/instructors.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | func (h *Handler) GetAllInstructors(c echo.Context) error { 11 | expertise := []string{} 12 | 13 | // the key was found. 14 | if err := echo.QueryParamsBinder(c).Strings("expertise", &expertise).BindError(); err != nil { //watch the == here 15 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 16 | } 17 | 18 | instructors, err := h.DB.GetAllInstructors() 19 | if err != nil { 20 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 21 | } 22 | 23 | return c.JSON(http.StatusOK, instructors) 24 | } 25 | 26 | func (h *Handler) GetInstructorByID(c echo.Context) error { 27 | id := -1 28 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 29 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 30 | } 31 | 32 | instructor, err := h.DB.GetInstructorByID(id) 33 | if err != nil { 34 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 35 | } 36 | 37 | if instructor == nil { 38 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("instructor with id : %d not found", id)) 39 | } 40 | 41 | return c.JSON(http.StatusOK, instructor) 42 | } 43 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/type.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/moficodes/restful-go-api/database/internal/datasource" 5 | ) 6 | 7 | type Handler struct { 8 | datasource.DB 9 | } 10 | 11 | type Message struct { 12 | Data string `json:"data"` 13 | } 14 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/users.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/database/internal/datasource" 9 | ) 10 | 11 | func (h *Handler) GetAllUsers(c echo.Context) error { 12 | users, err := h.DB.GetAllUsers() 13 | 14 | if err != nil { 15 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 16 | } 17 | 18 | return c.JSON(http.StatusOK, users) 19 | } 20 | 21 | func (h *Handler) GetUserByID(c echo.Context) error { 22 | id := -1 23 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 24 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 25 | } 26 | 27 | user, err := h.DB.GetUserByID(id) 28 | if err != nil { 29 | return echo.NewHTTPError(http.StatusInternalServerError, "error fetching data") 30 | } 31 | 32 | if user == nil { 33 | return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("user with id : %d not found", id)) 34 | } 35 | 36 | return c.JSON(http.StatusOK, user) 37 | } 38 | 39 | func (h *Handler) CreateNewUser(c echo.Context) error { 40 | c.Request().Header.Add("Content-Type", "application/json") 41 | if c.Request().ContentLength == 0 { 42 | return echo.NewHTTPError(http.StatusBadRequest, "body is required for this method") 43 | } 44 | user := new(datasource.User) 45 | err := c.Bind(user) 46 | if err != nil { 47 | return echo.NewHTTPError(http.StatusBadRequest, "body could not be parsed") 48 | } 49 | 50 | id, err := h.DB.CreateNewUser(user) 51 | if err != nil { 52 | return echo.NewHTTPError(http.StatusBadRequest, "could not create user") 53 | } 54 | 55 | return c.JSON(http.StatusCreated, Message{Data: fmt.Sprintf("user created with id : %d", id)}) 56 | } 57 | 58 | func (h *Handler) AddUserInterest(c echo.Context) error { 59 | c.Request().Header.Add("Content-Type", "application/json") 60 | 61 | id := -1 62 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 63 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 64 | } 65 | 66 | if c.Request().ContentLength == 0 { 67 | return echo.NewHTTPError(http.StatusBadRequest, "body is required for this method") 68 | } 69 | 70 | var interests []string 71 | err := c.Bind(&interests) 72 | if err != nil { 73 | return echo.NewHTTPError(http.StatusBadRequest, "body not be valid") 74 | } 75 | count, err := h.DB.AddUserInterest(id, interests) 76 | if err != nil { 77 | return echo.NewHTTPError(http.StatusBadRequest, "could not add user interest") 78 | } 79 | return c.JSON(http.StatusCreated, Message{Data: fmt.Sprintf("%d user interest added", count)}) 80 | } 81 | -------------------------------------------------------------------------------- /rest-api-database/internal/handler/util.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /rest-api-database/pkg/database/postgres.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jackc/pgx/v4/pgxpool" 7 | ) 8 | 9 | func PGPool(ctx context.Context, connStr string) (*pgxpool.Pool, error) { 10 | pool, err := pgxpool.Connect(ctx, connStr) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return pool, nil 15 | } 16 | -------------------------------------------------------------------------------- /rest-api-database/pkg/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | type CustomContext struct { 13 | echo.Context 14 | Claims jwt.MapClaims 15 | } 16 | 17 | func JWT(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | authorization := c.Request().Header.Get("Authorization") 20 | // auth token have the structure `bearer ` 21 | // so we split it on the ` ` (space character) 22 | splitToken := strings.Split(authorization, " ") 23 | // if we end up with a array of size 2 we have the token as the 24 | // 2nd item in the array 25 | if len(splitToken) != 2 { 26 | // we got something different 27 | return echo.NewHTTPError(http.StatusUnauthorized, "no valid token found") 28 | } 29 | // second item is our possible token 30 | jwtToken := splitToken[1] 31 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 32 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 33 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 34 | } 35 | return []byte("very-secret"), nil 36 | }) 37 | 38 | if err != nil { 39 | // we got something different 40 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 41 | } 42 | 43 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 44 | cc := &CustomContext{c, claims} 45 | return next(cc) 46 | 47 | } else { 48 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rest-api-database/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Logger(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | log.Println(c.Request().URL) 12 | return next(c) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /restapi-docs-echo/Readme.md: -------------------------------------------------------------------------------- 1 | # API Docs With Swagger/Open API 2 | 3 | Documentation is a core part of programming. Specially for REST APIs. Using documentation we let our users know how they can use our API. Go is a very readable (opinion) language. While anyone can read our code an have a good idea of how our API works, with a openapi spec we can do much better. 4 | 5 | ## What is Open API? 6 | 7 | The OpenAPI Specification (OAS) provides a consistent means to carry information through each stage of the API lifecycle. It is a specification language for HTTP APIs that defines structure and syntax in a way that is not wedded to the programming language the API is created in. API specifications are typically written in YAML or JSON, allowing for easy sharing and consumption of the specification. 8 | 9 | ## Getting Started 10 | 11 | ```bash 12 | git checkout origin/rest-api-docs-01 13 | ``` 14 | 15 | ## Open API Schema 16 | 17 | Open API is written in JSON or YAML. For example our schema for the API endpoint for getting a user with an id looks like this: 18 | 19 | ```yaml 20 | basePath: / 21 | definitions: 22 | handler.HTTPError: 23 | properties: 24 | message: {} 25 | type: object 26 | handler.User: 27 | properties: 28 | company: 29 | example: Acme Inc. 30 | type: string 31 | email: 32 | example: johndoe@gmail.com 33 | type: string 34 | id: 35 | example: 1 36 | type: integer 37 | interests: 38 | example: 39 | - golang 40 | - python 41 | items: 42 | type: string 43 | type: array 44 | name: 45 | example: John Doe 46 | type: string 47 | type: object 48 | host: localhost:7999 49 | paths: 50 | /api/v1/users/{id}: 51 | get: 52 | consumes: 53 | - '*/*' 54 | description: get user matching given ID. 55 | parameters: 56 | - description: user id 57 | in: path 58 | name: id 59 | required: true 60 | type: integer 61 | produces: 62 | - application/json 63 | responses: 64 | "200": 65 | description: OK 66 | schema: 67 | $ref: '#/definitions/handler.User' 68 | "404": 69 | description: Not Found 70 | schema: 71 | $ref: '#/definitions/handler.HTTPError' 72 | summary: Get user by id 73 | tags: 74 | - API 75 | schemes: 76 | - http 77 | swagger: "2.0" 78 | ``` 79 | 80 | ## Generating API Docs 81 | 82 | Writing these YAMLs by hand would be error prone. So we can make use of tools to generate it from comments for us. 83 | 84 | The same schema can be written in Go as: 85 | 86 | ```go 87 | // API godoc 88 | // @Summary Get user by id 89 | // @Description get user matching given ID. 90 | // @Tags API 91 | // @Accept */* 92 | // @Produce json 93 | // @Param id path int true "user id" 94 | // @Success 200 {object} User 95 | // @Failure 404 {object} HTTPError 96 | // @Router /api/v1/users/{id} [get] 97 | func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { 98 | // ... 99 | } 100 | ``` 101 | 102 | ## Swagger UI 103 | 104 | To generate and serve swagger UI we can use [swag]() and [echo-swagger](). 105 | 106 | ```bash 107 | swag init -g cmd/web/main.go --output docs/ 108 | ``` 109 | 110 | Then we run the application as normal 111 | 112 | ```bash 113 | go run cmd/web/main.go 114 | ``` 115 | 116 | And visit [http://localhost:7999/swagger](http://localhost:7999/swagger) -------------------------------------------------------------------------------- /restapi-docs-echo/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | echoMiddleware "github.com/labstack/echo/v4/middleware" 8 | "github.com/moficodes/restful-go-api/restapi-docs-echo/internal/handler" 9 | "github.com/moficodes/restful-go-api/restapi-docs-echo/pkg/middleware" 10 | echoSwagger "github.com/swaggo/echo-swagger" 11 | 12 | _ "github.com/moficodes/restful-go-api/restapi-docs-echo/docs" 13 | ) 14 | 15 | type server struct{} 16 | 17 | func Chain(h echo.HandlerFunc, middleware ...func(echo.HandlerFunc) echo.HandlerFunc) echo.HandlerFunc { 18 | for _, m := range middleware { 19 | h = m(h) 20 | } 21 | return h 22 | } 23 | 24 | // @title O'Reilly RESTful Go API Course 25 | // @version 1.0 26 | // @description This is a demo of openapi spec generation with Go. 27 | // @termsOfService http://swagger.io/terms/ 28 | 29 | // @contact.name Mofi Rahman 30 | // @contact.url http://www.swagger.io/support 31 | // @contact.email moficodes@gmail.com 32 | 33 | // @license.name Apache 2.0 34 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 35 | 36 | // @host localhost:7999 37 | // @BasePath / 38 | // @schemes http 39 | func main() { 40 | e := echo.New() 41 | 42 | specialLogger := echoMiddleware.LoggerWithConfig(echoMiddleware.LoggerConfig{ 43 | Format: "time=${time_rfc3339} method=${method}, uri=${uri}, status=${status}, latency=${latency_human}, \n", 44 | }) 45 | e.Use(middleware.Logger, specialLogger) 46 | e.Use(echoMiddleware.CORS()) 47 | e.Use(echoMiddleware.Recover()) 48 | 49 | e.GET("/", HealthCheck) 50 | e.GET("/swagger/*", echoSwagger.WrapHandler) 51 | 52 | auth := e.Group("/auth") 53 | auth.Use(middleware.JWT) 54 | auth.GET("/test", handler.Authenticated) 55 | api := e.Group("/api/v1") 56 | _ = Chain(handler.GetAllUsers, middleware.Logger, specialLogger) // this would give us a new handler that we can use in place of any other handler 57 | api.GET("/users", handler.GetAllUsers) 58 | api.GET("/instructors", handler.GetAllInstructors) 59 | api.GET("/courses", handler.GetAllCourses) 60 | 61 | api.GET("/users/:id", handler.GetUserByID) 62 | api.GET("/instructors/:id", handler.GetInstructorByID) 63 | api.GET("/courses/:id", handler.GetCoursesByID) 64 | port := "7999" 65 | 66 | e.Logger.Fatal(e.Start(":" + port)) 67 | } 68 | 69 | // HealthCheck godoc 70 | // @Summary Show the status of server. 71 | // @Description get the status of server. 72 | // @Tags root 73 | // @Accept */* 74 | // @Produce json 75 | // @Success 200 {object} map[string]interface{} 76 | // @Router / [get] 77 | func HealthCheck(c echo.Context) error { 78 | return c.JSON(http.StatusOK, map[string]interface{}{ 79 | "data": "Server is up and running", 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /restapi-docs-echo/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemes": [ 3 | "http" 4 | ], 5 | "swagger": "2.0", 6 | "info": { 7 | "description": "This is a sample server server.", 8 | "title": "Echo Swagger Example API", 9 | "termsOfService": "http://swagger.io/terms/", 10 | "contact": { 11 | "name": "API Support", 12 | "url": "http://www.swagger.io/support", 13 | "email": "support@swagger.io" 14 | }, 15 | "license": { 16 | "name": "Apache 2.0", 17 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 18 | }, 19 | "version": "1.0" 20 | }, 21 | "host": "localhost:7999", 22 | "basePath": "/", 23 | "paths": { 24 | "/": { 25 | "get": { 26 | "description": "get the status of server.", 27 | "consumes": [ 28 | "*/*" 29 | ], 30 | "produces": [ 31 | "application/json" 32 | ], 33 | "tags": [ 34 | "root" 35 | ], 36 | "summary": "Show the status of server.", 37 | "responses": { 38 | "200": { 39 | "description": "OK", 40 | "schema": { 41 | "type": "object", 42 | "additionalProperties": true 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | "/api/v1/users": { 49 | "get": { 50 | "description": "get all users matching given query params. returns all by default.", 51 | "consumes": [ 52 | "*/*" 53 | ], 54 | "produces": [ 55 | "application/json" 56 | ], 57 | "tags": [ 58 | "API" 59 | ], 60 | "summary": "Get all users", 61 | "parameters": [ 62 | { 63 | "type": "string", 64 | "description": "interests to filter by", 65 | "name": "interest", 66 | "in": "query" 67 | } 68 | ], 69 | "responses": { 70 | "200": { 71 | "description": "OK", 72 | "schema": { 73 | "type": "array", 74 | "items": { 75 | "$ref": "#/definitions/handler.User" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "/api/v1/users/{id}": { 83 | "get": { 84 | "description": "get user matching given ID.", 85 | "consumes": [ 86 | "*/*" 87 | ], 88 | "produces": [ 89 | "application/json" 90 | ], 91 | "tags": [ 92 | "API" 93 | ], 94 | "summary": "Get user by id", 95 | "parameters": [ 96 | { 97 | "type": "integer", 98 | "description": "user id", 99 | "name": "id", 100 | "in": "path", 101 | "required": true 102 | } 103 | ], 104 | "responses": { 105 | "200": { 106 | "description": "OK", 107 | "schema": { 108 | "$ref": "#/definitions/handler.User" 109 | } 110 | }, 111 | "404": { 112 | "description": "Not Found", 113 | "schema": { 114 | "$ref": "#/definitions/handler.HTTPError" 115 | } 116 | } 117 | } 118 | } 119 | }, 120 | "/auth/test": { 121 | "get": { 122 | "description": "get user info from the JWT token", 123 | "consumes": [ 124 | "*/*" 125 | ], 126 | "produces": [ 127 | "application/json" 128 | ], 129 | "tags": [ 130 | "authenticated" 131 | ], 132 | "summary": "Get user info from the JWT token", 133 | "responses": { 134 | "200": { 135 | "description": "OK", 136 | "schema": { 137 | "$ref": "#/definitions/handler.Message" 138 | } 139 | }, 140 | "401": { 141 | "description": "Unauthorized", 142 | "schema": { 143 | "type": "object", 144 | "additionalProperties": true 145 | } 146 | } 147 | } 148 | } 149 | } 150 | }, 151 | "definitions": { 152 | "handler.HTTPError": { 153 | "type": "object", 154 | "properties": { 155 | "message": {} 156 | } 157 | }, 158 | "handler.Message": { 159 | "type": "object", 160 | "properties": { 161 | "data": { 162 | "type": "string", 163 | "example": "John Doe" 164 | } 165 | } 166 | }, 167 | "handler.User": { 168 | "type": "object", 169 | "properties": { 170 | "company": { 171 | "type": "string", 172 | "example": "Acme Inc." 173 | }, 174 | "email": { 175 | "type": "string", 176 | "example": "johndoe@gmail.com" 177 | }, 178 | "id": { 179 | "type": "integer", 180 | "example": 1 181 | }, 182 | "interests": { 183 | "type": "array", 184 | "items": { 185 | "type": "string" 186 | }, 187 | "example": [ 188 | "golang", 189 | "python" 190 | ] 191 | }, 192 | "name": { 193 | "type": "string", 194 | "example": "John Doe" 195 | } 196 | } 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /restapi-docs-echo/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | handler.HTTPError: 4 | properties: 5 | message: {} 6 | type: object 7 | handler.Message: 8 | properties: 9 | data: 10 | example: John Doe 11 | type: string 12 | type: object 13 | handler.User: 14 | properties: 15 | company: 16 | example: Acme Inc. 17 | type: string 18 | email: 19 | example: johndoe@gmail.com 20 | type: string 21 | id: 22 | example: 1 23 | type: integer 24 | interests: 25 | example: 26 | - golang 27 | - python 28 | items: 29 | type: string 30 | type: array 31 | name: 32 | example: John Doe 33 | type: string 34 | type: object 35 | host: localhost:7999 36 | info: 37 | contact: 38 | email: support@swagger.io 39 | name: API Support 40 | url: http://www.swagger.io/support 41 | description: This is a sample server server. 42 | license: 43 | name: Apache 2.0 44 | url: http://www.apache.org/licenses/LICENSE-2.0.html 45 | termsOfService: http://swagger.io/terms/ 46 | title: Echo Swagger Example API 47 | version: "1.0" 48 | paths: 49 | /: 50 | get: 51 | consumes: 52 | - '*/*' 53 | description: get the status of server. 54 | produces: 55 | - application/json 56 | responses: 57 | "200": 58 | description: OK 59 | schema: 60 | additionalProperties: true 61 | type: object 62 | summary: Show the status of server. 63 | tags: 64 | - root 65 | /api/v1/users: 66 | get: 67 | consumes: 68 | - '*/*' 69 | description: get all users matching given query params. returns all by default. 70 | parameters: 71 | - description: interests to filter by 72 | in: query 73 | name: interest 74 | type: string 75 | produces: 76 | - application/json 77 | responses: 78 | "200": 79 | description: OK 80 | schema: 81 | items: 82 | $ref: '#/definitions/handler.User' 83 | type: array 84 | summary: Get all users 85 | tags: 86 | - API 87 | /api/v1/users/{id}: 88 | get: 89 | consumes: 90 | - '*/*' 91 | description: get user matching given ID. 92 | parameters: 93 | - description: user id 94 | in: path 95 | name: id 96 | required: true 97 | type: integer 98 | produces: 99 | - application/json 100 | responses: 101 | "200": 102 | description: OK 103 | schema: 104 | $ref: '#/definitions/handler.User' 105 | "404": 106 | description: Not Found 107 | schema: 108 | $ref: '#/definitions/handler.HTTPError' 109 | summary: Get user by id 110 | tags: 111 | - API 112 | /auth/test: 113 | get: 114 | consumes: 115 | - '*/*' 116 | description: get user info from the JWT token 117 | produces: 118 | - application/json 119 | responses: 120 | "200": 121 | description: OK 122 | schema: 123 | $ref: '#/definitions/handler.Message' 124 | "401": 125 | description: Unauthorized 126 | schema: 127 | additionalProperties: true 128 | type: object 129 | summary: Get user info from the JWT token 130 | tags: 131 | - authenticated 132 | schemes: 133 | - http 134 | swagger: "2.0" 135 | -------------------------------------------------------------------------------- /restapi-docs-echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/restapi-docs-echo 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/labstack/echo/v4 v4.9.0 8 | github.com/swaggo/swag v1.8.12 9 | ) 10 | 11 | require github.com/swaggo/files/v2 v2.0.0 // indirect 12 | 13 | require ( 14 | github.com/KyleBanks/depth v1.2.1 // indirect 15 | github.com/PuerkitoBio/purell v1.1.1 // indirect 16 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 17 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 18 | github.com/go-openapi/jsonreference v0.19.6 // indirect 19 | github.com/go-openapi/spec v0.20.4 // indirect 20 | github.com/go-openapi/swag v0.19.15 // indirect 21 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 22 | github.com/josharian/intern v1.0.0 // indirect 23 | github.com/labstack/gommon v0.3.1 // indirect 24 | github.com/mailru/easyjson v0.7.7 // indirect 25 | github.com/mattn/go-colorable v0.1.11 // indirect 26 | github.com/mattn/go-isatty v0.0.14 // indirect 27 | github.com/swaggo/echo-swagger v1.4.0 28 | github.com/valyala/bytebufferpool v1.0.0 // indirect 29 | github.com/valyala/fasttemplate v1.2.1 // indirect 30 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 31 | golang.org/x/net v0.8.0 // indirect 32 | golang.org/x/sys v0.6.0 // indirect 33 | golang.org/x/text v0.8.0 // indirect 34 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect 35 | golang.org/x/tools v0.7.0 // indirect 36 | gopkg.in/yaml.v2 v2.4.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/restapi-docs-echo/pkg/middleware" 9 | ) 10 | 11 | // Authenticated godoc 12 | // @Summary Get user info from the JWT token 13 | // @Description get user info from the JWT token 14 | // @Tags authenticated 15 | // @Accept */* 16 | // @Produce json 17 | // @Success 200 {object} Message 18 | // @Failure 401 {object} map[string]interface{} 19 | // @Router /auth/test [get] 20 | func Authenticated(c echo.Context) error { 21 | cc := c.(*middleware.CustomContext) 22 | _name, ok := cc.Claims["name"] 23 | if !ok { 24 | echo.NewHTTPError(http.StatusUnauthorized, "malformed jwt") 25 | } 26 | 27 | name := fmt.Sprintf("%v", _name) 28 | 29 | return c.JSON(http.StatusOK, Message{Data: name}) 30 | } 31 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/courses.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var courses []Course 11 | 12 | func init() { 13 | loadCourses("./data/courses.json") 14 | } 15 | 16 | func loadCourses(path string) { 17 | if courses != nil { 18 | return 19 | } 20 | if err := readContent(path, &courses); err != nil { 21 | log.Println("Could not read courses data") 22 | } 23 | } 24 | 25 | func GetAllCourses(c echo.Context) error { 26 | topics := []string{} 27 | attendees := []string{} 28 | instructor := -1 29 | 30 | if err := echo.QueryParamsBinder(c). 31 | Strings("topic", &topics). 32 | Int("instructor", &instructor). 33 | Strings("attendee", &attendees).BindError(); err != nil { 34 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 35 | } 36 | 37 | res := make([]Course, 0) 38 | for _, course := range courses { 39 | if contains(course.Topics, topics) && containsInt(course.Attendees, attendees) && (instructor == -1 || course.InstructorID == instructor) { 40 | res = append(res, course) 41 | } 42 | } 43 | return c.JSON(http.StatusOK, res) 44 | } 45 | 46 | func GetCoursesByID(c echo.Context) error { 47 | id := -1 48 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 49 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 50 | } 51 | 52 | var data *Course 53 | for _, v := range courses { 54 | if v.ID == id { 55 | data = &v 56 | break 57 | } 58 | } 59 | 60 | if data == nil { 61 | return echo.NewHTTPError(http.StatusNotFound, "course with id not found") 62 | } 63 | 64 | return c.JSON(http.StatusCreated, data) 65 | } 66 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/courses_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func init() { 14 | loadCourses("../../data/courses.json") 15 | } 16 | 17 | func TestGetAllCourses_success(t *testing.T) { 18 | w := httptest.NewRecorder() 19 | r := httptest.NewRequest(http.MethodGet, "/courses", nil) 20 | e := echo.New() 21 | c := e.NewContext(r, w) 22 | GetAllCourses(c) 23 | resp := w.Result() 24 | defer resp.Body.Close() 25 | 26 | body, err := ioutil.ReadAll(resp.Body) 27 | if err != nil { 28 | t.Errorf("readAll err=%s; want nil", err) 29 | } 30 | var res []Course 31 | err = json.Unmarshal(body, &res) 32 | if err != nil { 33 | t.Errorf("unmarshal err=%s; want nil", err) 34 | } 35 | 36 | want := 100 37 | got := len(res) 38 | 39 | if err != nil { 40 | t.Errorf("want=%d; got=%d", want, got) 41 | } 42 | } 43 | 44 | func TestGetCoursesByID_success(t *testing.T) { 45 | w := httptest.NewRecorder() 46 | r := httptest.NewRequest(http.MethodGet, "/", nil) 47 | e := echo.New() 48 | 49 | c := e.NewContext(r, w) 50 | c.SetPath("/courses/:id") 51 | c.SetParamNames("id") 52 | c.SetParamValues("1") 53 | 54 | GetCoursesByID(c) 55 | 56 | resp := w.Result() 57 | defer resp.Body.Close() 58 | 59 | body, err := ioutil.ReadAll(resp.Body) 60 | if err != nil { 61 | t.Errorf("readAll err=%s; want nil", err) 62 | } 63 | var res Course 64 | err = json.Unmarshal(body, &res) 65 | if err != nil { 66 | t.Errorf("unmarshal err=%s; want nil", err) 67 | } 68 | 69 | want := 1 70 | got := res.ID 71 | 72 | if want != got { 73 | t.Errorf("want=1; got=%d", got) 74 | } 75 | 76 | want = 201 77 | got = w.Code 78 | 79 | if want != got { 80 | t.Errorf("want=%d; got=%d", want, got) 81 | } 82 | } 83 | 84 | func TestGetCoursesByID_failure(t *testing.T) { 85 | w := httptest.NewRecorder() 86 | r := httptest.NewRequest(http.MethodGet, "/", nil) 87 | 88 | e := echo.New() 89 | 90 | c := e.NewContext(r, w) 91 | c.SetPath("/courses/:id") 92 | c.SetParamNames("id") 93 | c.SetParamValues("101") 94 | err := GetCoursesByID(c) 95 | he, ok := err.(*echo.HTTPError) 96 | if !ok { 97 | t.Error("should be http error") 98 | } 99 | 100 | want := 404 101 | got := he.Code 102 | if want != got { 103 | t.Errorf("want=%d; got=%d", want, got) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/instructors.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var instructors []Instructor 11 | 12 | func init() { 13 | loadInstructors("./data/instructors.json") 14 | } 15 | 16 | func loadInstructors(path string) { 17 | if err := readContent(path, &instructors); err != nil { 18 | log.Println("Could not read instructors data") 19 | } 20 | } 21 | 22 | func GetAllInstructors(c echo.Context) error { 23 | expertise := []string{} 24 | 25 | // the key was found. 26 | if err := echo.QueryParamsBinder(c).Strings("expertise", &expertise).BindError(); err != nil { //watch the == here 27 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 28 | } 29 | res := make([]Instructor, 0) 30 | for _, instructor := range instructors { 31 | if contains(instructor.Expertise, expertise) { 32 | res = append(res, instructor) 33 | } 34 | } 35 | return c.JSON(http.StatusOK, res) 36 | } 37 | 38 | func GetInstructorByID(c echo.Context) error { 39 | id := -1 40 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 41 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 42 | } 43 | 44 | var data *Instructor 45 | for _, v := range instructors { 46 | if v.ID == id { 47 | data = &v 48 | break 49 | } 50 | } 51 | 52 | if data == nil { 53 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 54 | } 55 | 56 | return c.JSON(http.StatusOK, data) 57 | } 58 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/type.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | type User struct { 4 | ID int `json:"id" example:"1"` 5 | Name string `json:"name" example:"John Doe"` 6 | Email string `json:"email" example:"johndoe@gmail.com"` 7 | Company string `json:"company" example:"Acme Inc."` 8 | Interests []string `json:"interests" example:"golang,python"` 9 | } 10 | 11 | // Instructor type represent a instructor for a course 12 | type Instructor struct { 13 | ID int `json:"id"` 14 | Name string `json:"name"` 15 | Email string `json:"email"` 16 | Company string `json:"company"` 17 | Expertise []string `json:"expertise"` 18 | } 19 | 20 | // Course is course being taught 21 | type Course struct { 22 | ID int `json:"id"` 23 | InstructorID int `json:"instructor_id"` 24 | Name string `json:"name"` 25 | Topics []string `json:"topics"` 26 | Attendees []int `json:"attendees"` 27 | } 28 | 29 | type Message struct { 30 | Data string `json:"data" example:"John Doe"` 31 | } 32 | 33 | type HTTPError struct { 34 | Code int `json:"-"` 35 | Message interface{} `json:"message"` 36 | Internal error `json:"-"` // Stores the error returned by an external dependency 37 | } 38 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/users.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var users []User 11 | 12 | func init() { 13 | loadUsers("./data/users.json") 14 | } 15 | 16 | func loadUsers(path string) { 17 | if err := readContent(path, &users); err != nil { 18 | log.Println("Could not read instructors data") 19 | } 20 | } 21 | 22 | // API godoc 23 | // @Summary Get all users 24 | // @Description get all users matching given query params. returns all by default. 25 | // @Tags API 26 | // @Accept */* 27 | // @Produce json 28 | // @Param interest query string false "interests to filter by" 29 | // @Success 200 {object} []User 30 | // @Router /api/v1/users [get] 31 | func GetAllUsers(c echo.Context) error { 32 | interests := []string{} 33 | if err := echo.QueryParamsBinder(c).Strings("interest", &interests).BindError(); err != nil { 34 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 35 | } 36 | 37 | res := make([]User, 0) 38 | for _, user := range users { 39 | if contains(user.Interests, interests) { 40 | res = append(res, user) 41 | } 42 | } 43 | 44 | return c.JSON(http.StatusOK, res) 45 | } 46 | 47 | // API godoc 48 | // @Summary Get user by id 49 | // @Description get user matching given ID. 50 | // @Tags API 51 | // @Accept */* 52 | // @Produce json 53 | // @Param id path int true "user id" 54 | // @Success 200 {object} User 55 | // @Failure 404 {object} HTTPError 56 | // @Router /api/v1/users/{id} [get] 57 | func GetUserByID(c echo.Context) error { 58 | id := -1 59 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 60 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 61 | } 62 | 63 | var data *User 64 | for _, v := range users { 65 | if v.ID == id { 66 | data = &v 67 | break 68 | } 69 | } 70 | 71 | if data == nil { 72 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 73 | } 74 | 75 | return c.JSON(http.StatusOK, data) 76 | } 77 | -------------------------------------------------------------------------------- /restapi-docs-echo/internal/handler/util.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /restapi-docs-echo/pkg/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | type CustomContext struct { 13 | echo.Context 14 | Claims jwt.MapClaims 15 | } 16 | 17 | func JWT(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | authorization := c.Request().Header.Get("Authorization") 20 | // auth token have the structure `bearer ` 21 | // so we split it on the ` ` (space character) 22 | splitToken := strings.Split(authorization, " ") 23 | // if we end up with a array of size 2 we have the token as the 24 | // 2nd item in the array 25 | if len(splitToken) != 2 { 26 | // we got something different 27 | return echo.NewHTTPError(http.StatusUnauthorized, "no valid token found") 28 | } 29 | // second item is our possible token 30 | jwtToken := splitToken[1] 31 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 32 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 33 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 34 | } 35 | return []byte("very-secret"), nil 36 | }) 37 | 38 | if err != nil { 39 | // we got something different 40 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 41 | } 42 | 43 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 44 | cc := &CustomContext{c, claims} 45 | return next(cc) 46 | 47 | } else { 48 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /restapi-docs-echo/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Logger(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | log.Println(c.Request().URL) 12 | return next(c) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testing-benchmark-echo/Readme.md: -------------------------------------------------------------------------------- 1 | # Testing and Benchmarking 2 | 3 | ## Testing 4 | 5 | There are two main kinds of tests 6 | 7 | 1. Functional Testing types 8 | - Unit Testing 9 | - Integration Testing 10 | - System Testing 11 | - Sanity Testing 12 | - Smoke Testing 13 | - Interface Testing 14 | - Regression Testing 15 | - Beta/Acceptance Testing 16 | 17 | 2. Non-functional Testing 18 | - Performance Testing 19 | - Load Testing 20 | - Stress Testing 21 | - Volume Testing 22 | - Security Testing 23 | - Compatibility Testing 24 | - Install Testing 25 | - Recovery Testing 26 | - Reliability Testing 27 | - Usability Testing 28 | - Compliance Testing 29 | - Localization Testing 30 | 31 | We will be mainly looking at unit testing our REST API in this section. Some of these testing are out of scope for our code altogether. For example, System Testing or Regression Testing. 32 | 33 | ## Unit Testing 34 | 35 | Unit test checks the functionality of single unit of code and confirms agains know output that the result is consistent. 36 | 37 | Go has a built-in package [httptest](https://golang.org/pkg/net/http/httptest) for testing http methods. 38 | 39 | Look at `courses_test.go` for some examples of http tests in go. I leave writing test for the other handlers as an exercise you you. 40 | 41 | ## Running the Test 42 | 43 | To run the code in this section 44 | 45 | ```bash 46 | git checkout origin/testing-benchmarking-echo-01 47 | ``` 48 | 49 | If you are not already in the folder 50 | 51 | ```bash 52 | cd testing-benchmark-echo 53 | ``` 54 | 55 | ```bash 56 | go test -v ./... 57 | ``` 58 | 59 | > If you return a custom error from echo the `ResponseRecorder` will not actually get the error code updated. That's why we need to cast the error to `echo.HttpError` to get the error code. -------------------------------------------------------------------------------- /testing-benchmark-echo/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | echoMiddleware "github.com/labstack/echo/v4/middleware" 6 | "github.com/moficodes/restful-go-api/echo-testing/internal/handler" 7 | "github.com/moficodes/restful-go-api/echo-testing/pkg/middleware" 8 | ) 9 | 10 | type server struct{} 11 | 12 | func Chain(h echo.HandlerFunc, middleware ...func(echo.HandlerFunc) echo.HandlerFunc) echo.HandlerFunc { 13 | for _, m := range middleware { 14 | h = m(h) 15 | } 16 | return h 17 | } 18 | 19 | func main() { 20 | e := echo.New() 21 | 22 | specialLogger := echoMiddleware.LoggerWithConfig(echoMiddleware.LoggerConfig{ 23 | Format: "time=${time_rfc3339} method=${method}, uri=${uri}, status=${status}, latency=${latency_human}, \n", 24 | }) 25 | e.Use(middleware.Logger, specialLogger) 26 | 27 | auth := e.Group("/auth") 28 | auth.Use(middleware.JWT) 29 | auth.GET("/test", handler.Authenticated) 30 | api := e.Group("/api/v1") 31 | _ = Chain(handler.GetAllUsers, middleware.Logger, specialLogger) // this would give us a new handler that we can use in place of any other handler 32 | api.GET("/users", handler.GetAllUsers) 33 | api.GET("/instructors", handler.GetAllInstructors) 34 | api.GET("/courses", handler.GetAllCourses) 35 | 36 | api.GET("/users/:id", handler.GetUserByID) 37 | api.GET("/instructors/:id", handler.GetInstructorByID) 38 | api.GET("/courses/:id", handler.GetCoursesByID) 39 | port := "7999" 40 | 41 | e.Logger.Fatal(e.Start(":" + port)) 42 | } 43 | -------------------------------------------------------------------------------- /testing-benchmark-echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/echo-testing 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/labstack/echo/v4 v4.2.2 8 | github.com/mattn/go-colorable v0.1.8 // indirect 9 | golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc // indirect 10 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d // indirect 11 | golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /testing-benchmark-echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 4 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 5 | github.com/labstack/echo/v4 v4.2.2 h1:bq2fdZCionY1jck8rzUpQEu2YSmI8QbX6LHrCa60IVs= 6 | github.com/labstack/echo/v4 v4.2.2/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= 7 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 8 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 9 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 10 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 11 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 12 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 13 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 14 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 15 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 16 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 21 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 22 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 23 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 24 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 25 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 26 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 27 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 28 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 29 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 30 | golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA= 31 | golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 32 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 33 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 34 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 35 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= 36 | golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4= 48 | golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 50 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 51 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 52 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 53 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 54 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= 55 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 59 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/moficodes/restful-go-api/echo-testing/pkg/middleware" 9 | ) 10 | 11 | func Authenticated(c echo.Context) error { 12 | cc := c.(*middleware.CustomContext) 13 | _name, ok := cc.Claims["name"] 14 | if !ok { 15 | echo.NewHTTPError(http.StatusUnauthorized, "malformed jwt") 16 | } 17 | 18 | name := fmt.Sprintf("%v", _name) 19 | 20 | return c.JSON(http.StatusOK, Message{Data: name}) 21 | } 22 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/courses.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var courses []Course 11 | 12 | func init() { 13 | loadCourses("./data/courses.json") 14 | } 15 | 16 | func loadCourses(path string) { 17 | if courses != nil { 18 | return 19 | } 20 | if err := readContent(path, &courses); err != nil { 21 | log.Println("Could not read courses data") 22 | } 23 | } 24 | 25 | func GetAllCourses(c echo.Context) error { 26 | topics := []string{} 27 | attendees := []string{} 28 | instructor := -1 29 | 30 | if err := echo.QueryParamsBinder(c). 31 | Strings("topic", &topics). 32 | Int("instructor", &instructor). 33 | Strings("attendee", &attendees).BindError(); err != nil { 34 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 35 | } 36 | 37 | res := make([]Course, 0) 38 | for _, course := range courses { 39 | if contains(course.Topics, topics) && containsInt(course.Attendees, attendees) && (instructor == -1 || course.InstructorID == instructor) { 40 | res = append(res, course) 41 | } 42 | } 43 | return c.JSON(http.StatusOK, res) 44 | } 45 | 46 | func GetCoursesByID(c echo.Context) error { 47 | id := -1 48 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 49 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 50 | } 51 | 52 | var data *Course 53 | for _, v := range courses { 54 | if v.ID == id { 55 | data = &v 56 | break 57 | } 58 | } 59 | 60 | if data == nil { 61 | return echo.NewHTTPError(http.StatusNotFound, "course with id not found") 62 | } 63 | 64 | return c.JSON(http.StatusCreated, data) 65 | } 66 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/courses_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | func init() { 14 | loadCourses("../../data/courses.json") 15 | } 16 | 17 | func TestGetAllCourses_success(t *testing.T) { 18 | w := httptest.NewRecorder() 19 | r := httptest.NewRequest(http.MethodGet, "/courses", nil) 20 | e := echo.New() 21 | c := e.NewContext(r, w) 22 | GetAllCourses(c) 23 | resp := w.Result() 24 | defer resp.Body.Close() 25 | 26 | body, err := ioutil.ReadAll(resp.Body) 27 | if err != nil { 28 | t.Errorf("readAll err=%s; want nil", err) 29 | } 30 | var res []Course 31 | err = json.Unmarshal(body, &res) 32 | if err != nil { 33 | t.Errorf("unmarshal err=%s; want nil", err) 34 | } 35 | 36 | want := 100 37 | got := len(res) 38 | 39 | if err != nil { 40 | t.Errorf("want=%d; got=%d", want, got) 41 | } 42 | } 43 | 44 | func TestGetCoursesByID_success(t *testing.T) { 45 | w := httptest.NewRecorder() 46 | r := httptest.NewRequest(http.MethodGet, "/", nil) 47 | e := echo.New() 48 | 49 | c := e.NewContext(r, w) 50 | c.SetPath("/courses/:id") 51 | c.SetParamNames("id") 52 | c.SetParamValues("1") 53 | 54 | GetCoursesByID(c) 55 | 56 | resp := w.Result() 57 | defer resp.Body.Close() 58 | 59 | body, err := ioutil.ReadAll(resp.Body) 60 | if err != nil { 61 | t.Errorf("readAll err=%s; want nil", err) 62 | } 63 | var res Course 64 | err = json.Unmarshal(body, &res) 65 | if err != nil { 66 | t.Errorf("unmarshal err=%s; want nil", err) 67 | } 68 | 69 | want := 1 70 | got := res.ID 71 | 72 | if want != got { 73 | t.Errorf("want=1; got=%d", got) 74 | } 75 | 76 | want = 201 77 | got = w.Code 78 | 79 | if want != got { 80 | t.Errorf("want=%d; got=%d", want, got) 81 | } 82 | } 83 | 84 | func TestGetCoursesByID_failure(t *testing.T) { 85 | w := httptest.NewRecorder() 86 | r := httptest.NewRequest(http.MethodGet, "/", nil) 87 | 88 | e := echo.New() 89 | 90 | c := e.NewContext(r, w) 91 | c.SetPath("/courses/:id") 92 | c.SetParamNames("id") 93 | c.SetParamValues("101") 94 | err := GetCoursesByID(c) 95 | he, ok := err.(*echo.HTTPError) 96 | if !ok { 97 | t.Error("should be http error") 98 | } 99 | 100 | want := 404 101 | got := he.Code 102 | if want != got { 103 | t.Errorf("want=%d; got=%d", want, got) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/instructors.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var instructors []Instructor 11 | 12 | func init() { 13 | loadInstructors("./data/instructors.json") 14 | } 15 | 16 | func loadInstructors(path string) { 17 | if err := readContent(path, &instructors); err != nil { 18 | log.Println("Could not read instructors data") 19 | } 20 | } 21 | 22 | func GetAllInstructors(c echo.Context) error { 23 | expertise := []string{} 24 | 25 | // the key was found. 26 | if err := echo.QueryParamsBinder(c).Strings("expertise", &expertise).BindError(); err != nil { //watch the == here 27 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 28 | } 29 | res := make([]Instructor, 0) 30 | for _, instructor := range instructors { 31 | if contains(instructor.Expertise, expertise) { 32 | res = append(res, instructor) 33 | } 34 | } 35 | return c.JSON(http.StatusOK, res) 36 | } 37 | 38 | func GetInstructorByID(c echo.Context) error { 39 | id := -1 40 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 41 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 42 | } 43 | 44 | var data *Instructor 45 | for _, v := range instructors { 46 | if v.ID == id { 47 | data = &v 48 | break 49 | } 50 | } 51 | 52 | if data == nil { 53 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 54 | } 55 | 56 | return c.JSON(http.StatusOK, data) 57 | } 58 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/type.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | type User struct { 4 | ID int `json:"id"` 5 | Name string `json:"name"` 6 | Email string `json:"email"` 7 | Company string `json:"company"` 8 | Interests []string `json:"interests"` 9 | } 10 | 11 | // Instructor type represent a instructor for a course 12 | type Instructor struct { 13 | ID int `json:"id"` 14 | Name string `json:"name"` 15 | Email string `json:"email"` 16 | Company string `json:"company"` 17 | Expertise []string `json:"expertise"` 18 | } 19 | 20 | // Course is course being taught 21 | type Course struct { 22 | ID int `json:"id"` 23 | InstructorID int `json:"instructor_id"` 24 | Name string `json:"name"` 25 | Topics []string `json:"topics"` 26 | Attendees []int `json:"attendees"` 27 | } 28 | 29 | type Message struct { 30 | Data string `json:"data"` 31 | } 32 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/users.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | var users []User 11 | 12 | func init() { 13 | loadUsers("./data/users.json") 14 | } 15 | 16 | func loadUsers(path string) { 17 | if err := readContent(path, &users); err != nil { 18 | log.Println("Could not read instructors data") 19 | } 20 | } 21 | 22 | func GetAllUsers(c echo.Context) error { 23 | interests := []string{} 24 | if err := echo.QueryParamsBinder(c).Strings("interest", &interests).BindError(); err != nil { 25 | return echo.NewHTTPError(http.StatusBadRequest, "incorrect usage of query param") 26 | } 27 | 28 | res := make([]User, 0) 29 | for _, user := range users { 30 | if contains(user.Interests, interests) { 31 | res = append(res, user) 32 | } 33 | } 34 | 35 | return c.JSON(http.StatusOK, res) 36 | } 37 | 38 | func GetUserByID(c echo.Context) error { 39 | id := -1 40 | if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { 41 | return echo.NewHTTPError(http.StatusBadRequest, "invalid path param") 42 | } 43 | 44 | var data *User 45 | for _, v := range users { 46 | if v.ID == id { 47 | data = &v 48 | break 49 | } 50 | } 51 | 52 | if data == nil { 53 | return echo.NewHTTPError(http.StatusNotFound, "user with id not found") 54 | } 55 | 56 | return c.JSON(http.StatusOK, data) 57 | } 58 | -------------------------------------------------------------------------------- /testing-benchmark-echo/internal/handler/util.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /testing-benchmark-echo/pkg/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | type CustomContext struct { 13 | echo.Context 14 | Claims jwt.MapClaims 15 | } 16 | 17 | func JWT(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | authorization := c.Request().Header.Get("Authorization") 20 | // auth token have the structure `bearer ` 21 | // so we split it on the ` ` (space character) 22 | splitToken := strings.Split(authorization, " ") 23 | // if we end up with a array of size 2 we have the token as the 24 | // 2nd item in the array 25 | if len(splitToken) != 2 { 26 | // we got something different 27 | return echo.NewHTTPError(http.StatusUnauthorized, "no valid token found") 28 | } 29 | // second item is our possible token 30 | jwtToken := splitToken[1] 31 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 32 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 33 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 34 | } 35 | return []byte("very-secret"), nil 36 | }) 37 | 38 | if err != nil { 39 | // we got something different 40 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 41 | } 42 | 43 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 44 | cc := &CustomContext{c, claims} 45 | return next(cc) 46 | 47 | } else { 48 | return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized") 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /testing-benchmark-echo/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func Logger(next echo.HandlerFunc) echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | log.Println(c.Request().URL) 12 | return next(c) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testing-benchmark/Readme.md: -------------------------------------------------------------------------------- 1 | # Testing and Benchmarking 2 | 3 | ## Testing 4 | 5 | There are two main kinds of tests 6 | 7 | 1. Functional Testing types 8 | - Unit Testing 9 | - Integration Testing 10 | - System Testing 11 | - Sanity Testing 12 | - Smoke Testing 13 | - Interface Testing 14 | - Regression Testing 15 | - Beta/Acceptance Testing 16 | 17 | 2. Non-functional Testing 18 | - Performance Testing 19 | - Load Testing 20 | - Stress Testing 21 | - Volume Testing 22 | - Security Testing 23 | - Compatibility Testing 24 | - Install Testing 25 | - Recovery Testing 26 | - Reliability Testing 27 | - Usability Testing 28 | - Compliance Testing 29 | - Localization Testing 30 | 31 | We will be mainly looking at unit testing our REST API in this section. Some of these testing are out of scope for our code altogether. For example, System Testing or Regression Testing. 32 | 33 | ## Unit Testing 34 | 35 | Unit test checks the functionality of single unit of code and confirms agains know output that the result is consistent. 36 | 37 | Go has a built-in package [httptest](https://golang.org/pkg/net/http/httptest) for testing http methods. 38 | 39 | Look at `courses_test.go` for some examples of http tests in go. I leave writing test for the other handlers as an exercise you you. 40 | 41 | ## Running the Test 42 | 43 | To run the code in this section 44 | 45 | ```bash 46 | git checkout origin/testing-benchmarking-01 47 | ``` 48 | 49 | If you are not already in the folder 50 | 51 | ```bash 52 | cd testing-benchmark 53 | ``` 54 | 55 | ```bash 56 | go test -v ./... 57 | ``` 58 | 59 | 60 | -------------------------------------------------------------------------------- /testing-benchmark/cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | 9 | "github.com/moficodes/restful-go-api/testing-benchmark/internal/handler" 10 | internalMiddleware "github.com/moficodes/restful-go-api/testing-benchmark/internal/middleware" 11 | "github.com/moficodes/restful-go-api/testing-benchmark/pkg/middleware" 12 | ) 13 | 14 | func main() { 15 | s := &handler.Server{Routes: make([]string, 0)} 16 | r := mux.NewRouter() 17 | 18 | r.Handle("/", s) 19 | api := r.PathPrefix("/api/v1").Subrouter() 20 | auth := r.PathPrefix("/auth").Subrouter() 21 | auth.Use(internalMiddleware.JWTAuth) 22 | auth.HandleFunc("/check", handler.AuthHandler) 23 | 24 | api.Use(middleware.Time) 25 | 26 | api.HandleFunc("/users", handler.GetAllUsers).Methods(http.MethodGet) 27 | 28 | api.HandleFunc("/courses", handler.GetCoursesWithInstructorAndAttendee). 29 | Queries("instructor", "{instructor:[0-9]+}", "attendee", "{attendee:[0-9]+}"). 30 | Methods(http.MethodGet) 31 | 32 | api.HandleFunc("/courses", handler.GetAllCourses).Methods(http.MethodGet) 33 | api.HandleFunc("/instructors", handler.GetAllInstructors).Methods(http.MethodGet) 34 | 35 | // in gorilla mux we can name path parameters 36 | // the library will put them in an key,val map for us 37 | api.HandleFunc("/users/{id}", handler.GetUserByID).Methods(http.MethodGet) 38 | api.HandleFunc("/courses/{id}", handler.GetCoursesByID).Methods(http.MethodGet) 39 | api.HandleFunc("/instructors/{id}", handler.GetInstructorByID).Methods(http.MethodGet) 40 | 41 | port := "7999" 42 | log.Println("starting web server on port", port) 43 | 44 | r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 45 | t, err := route.GetPathTemplate() 46 | if err != nil { 47 | return err 48 | } 49 | s.Routes = append(s.Routes, t) 50 | return nil 51 | }) 52 | log.Println("available routes: ", s.Routes) 53 | // instead of using the default handler that comes with net/http we use the mux router from gorilla mux 54 | log.Fatal(http.ListenAndServe(":"+port, middleware.Chain(r, middleware.MuxLogger, middleware.Logger))) 55 | } 56 | -------------------------------------------------------------------------------- /testing-benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/moficodes/restful-go-api/testing-benchmark 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gorilla/handlers v1.4.2 8 | github.com/gorilla/mux v1.7.4 9 | ) 10 | -------------------------------------------------------------------------------- /testing-benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 4 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 5 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 6 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 7 | github.com/moficodes/restful-go-api v0.0.0-20200813052054-f2cda0febbb7 h1:ein3HNIXWad9JXDCJKNhyABefkJYC1Ce7JvkEdD1x9o= 8 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/authenticated.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/moficodes/restful-go-api/testing-benchmark/internal/middleware" 9 | ) 10 | 11 | func AuthHandler(w http.ResponseWriter, r *http.Request) { 12 | data := r.Context().Value(middleware.ContextKey("props")).(jwt.MapClaims) 13 | 14 | name, ok := data["name"] 15 | if !ok { 16 | w.WriteHeader(http.StatusUnauthorized) 17 | w.Write([]byte(`{"error": "not authorized"}`)) 18 | return 19 | } 20 | w.WriteHeader(http.StatusOK) 21 | w.Write([]byte(fmt.Sprintf("{\"message\": \"hello %v\"}", name))) 22 | } 23 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/course.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | courses []Course 14 | ) 15 | 16 | func init() { 17 | loadCourses("./data/courses.json") 18 | } 19 | 20 | func loadCourses(path string) { 21 | if courses != nil { 22 | return 23 | } 24 | if err := readContent(path, &courses); err != nil { 25 | log.Println("Could not read courses data") 26 | } 27 | } 28 | 29 | func GetAllCourses(w http.ResponseWriter, r *http.Request) { 30 | w.Header().Add("Content-Type", "application/json") 31 | query := r.URL.Query() 32 | topics, ok := query["topic"] 33 | if ok { 34 | res := make([]Course, 0) 35 | for _, course := range courses { 36 | if contains(course.Topics, topics) { 37 | res = append(res, course) 38 | } 39 | } 40 | 41 | e := json.NewEncoder(w) 42 | e.Encode(res) 43 | return 44 | } 45 | 46 | e := json.NewEncoder(w) 47 | e.Encode(courses) 48 | } 49 | 50 | func GetCoursesWithInstructorAndAttendee(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Add("Content-Type", "application/json") 52 | // we don't have to check for multiple instructor because the way our data is structured 53 | // there is no way multiple instructor can be part of same course 54 | _instructor := r.URL.Query().Get("instructor") 55 | instructorID, _ := strconv.Atoi(_instructor) 56 | // but multiple attendee can be part of the same course 57 | // since we gurrantee only valid integer queries will be sent to this route 58 | // we don't need to check if there is value or not. 59 | attendees := r.URL.Query()["attendee"] 60 | res := make([]Course, 0) 61 | 62 | for _, course := range courses { 63 | if course.InstructorID == instructorID && containsInt(course.Attendees, attendees) { 64 | res = append(res, course) 65 | } 66 | } 67 | 68 | e := json.NewEncoder(w) 69 | e.Encode(res) 70 | } 71 | 72 | func GetCoursesByID(w http.ResponseWriter, r *http.Request) { 73 | w.Header().Add("Content-Type", "application/json") 74 | // this function takes in the request and parses 75 | // all the pathParams from it 76 | // pathParams is a map[string]string 77 | pathParams := mux.Vars(r) 78 | id := -1 79 | var err error 80 | if val, ok := pathParams["id"]; ok { 81 | id, err = strconv.Atoi(val) 82 | if err != nil { 83 | w.WriteHeader(http.StatusBadRequest) 84 | w.Write([]byte(`{"error": "need a valid id"}`)) 85 | return 86 | } 87 | } 88 | 89 | var data *Course 90 | for _, v := range courses { 91 | if v.ID == id { 92 | data = &v 93 | break 94 | } 95 | } 96 | 97 | if data == nil { 98 | w.WriteHeader(http.StatusNotFound) 99 | w.Write([]byte(`{"error": "not found"}`)) 100 | } 101 | 102 | e := json.NewEncoder(w) 103 | e.Encode(data) 104 | } 105 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/course_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | func init() { 14 | loadCourses("../../data/courses.json") 15 | } 16 | 17 | func TestGetAllCourses_success(t *testing.T) { 18 | w := httptest.NewRecorder() 19 | r := httptest.NewRequest(http.MethodGet, "/courses", nil) 20 | 21 | GetAllCourses(w, r) 22 | resp := w.Result() 23 | defer resp.Body.Close() 24 | 25 | body, err := ioutil.ReadAll(resp.Body) 26 | if err != nil { 27 | t.Errorf("readAll err=%s; want nil", err) 28 | } 29 | var res []Course 30 | err = json.Unmarshal(body, &res) 31 | if err != nil { 32 | t.Errorf("unmarshal err=%s; want nil", err) 33 | } 34 | 35 | want := 100 36 | got := len(res) 37 | 38 | if err != nil { 39 | t.Errorf("want=%d; got=%d", want, got) 40 | } 41 | } 42 | 43 | func TestGetAllCourses_server(t *testing.T) { 44 | ts := httptest.NewServer(http.HandlerFunc(GetAllCourses)) 45 | defer ts.Close() 46 | 47 | resp, err := http.Get(ts.URL) 48 | if err != nil { 49 | t.Errorf("get error=%s, wanted nil", err) 50 | } 51 | 52 | body, err := ioutil.ReadAll(resp.Body) 53 | if err != nil { 54 | t.Errorf("readAll err=%s; want nil", err) 55 | } 56 | var res []Course 57 | err = json.Unmarshal(body, &res) 58 | if err != nil { 59 | t.Errorf("unmarshal err=%s; want nil", err) 60 | } 61 | want := 100 62 | got := len(res) 63 | 64 | if err != nil { 65 | t.Errorf("want=%d; got=%d", want, got) 66 | } 67 | } 68 | 69 | func TestGetCoursesByID_success(t *testing.T) { 70 | w := httptest.NewRecorder() 71 | r := httptest.NewRequest(http.MethodGet, "/courses/1", nil) 72 | 73 | vars := map[string]string{ 74 | "id": "1", 75 | } 76 | 77 | r = mux.SetURLVars(r, vars) 78 | 79 | GetCoursesByID(w, r) 80 | resp := w.Result() 81 | defer resp.Body.Close() 82 | 83 | body, err := ioutil.ReadAll(resp.Body) 84 | if err != nil { 85 | t.Errorf("readAll err=%s; want nil", err) 86 | } 87 | var res Course 88 | err = json.Unmarshal(body, &res) 89 | if err != nil { 90 | t.Errorf("unmarshal err=%s; want nil", err) 91 | } 92 | 93 | want := 1 94 | got := res.ID 95 | 96 | if want != got { 97 | t.Errorf("want=1; got=%d", got) 98 | } 99 | } 100 | 101 | func TestGetCoursesByID_failure(t *testing.T) { 102 | w := httptest.NewRecorder() 103 | r := httptest.NewRequest(http.MethodGet, "/courses/101", nil) 104 | 105 | vars := map[string]string{ 106 | "id": "101", 107 | } 108 | 109 | r = mux.SetURLVars(r, vars) 110 | 111 | GetCoursesByID(w, r) 112 | resp := w.Result() 113 | want := 404 114 | got := resp.StatusCode 115 | if want != got { 116 | t.Errorf("want=%d; got=%d", want, got) 117 | } 118 | } 119 | 120 | func TestGetCoursesWithInstructorAndAttendee_success(t *testing.T) { 121 | w := httptest.NewRecorder() 122 | r := httptest.NewRequest(http.MethodGet, "/?instructor=1&attendee=2", nil) 123 | 124 | GetCoursesWithInstructorAndAttendee(w, r) 125 | resp := w.Result() 126 | 127 | body, err := ioutil.ReadAll(resp.Body) 128 | if err != nil { 129 | t.Errorf("readAll err=%s; want nil", err) 130 | } 131 | 132 | var res []Course 133 | err = json.Unmarshal(body, &res) 134 | if err != nil { 135 | t.Errorf("unmarshal err=%s; want nil", err) 136 | } 137 | want := 1 138 | got := len(res) 139 | 140 | if err != nil { 141 | t.Errorf("want=%d; got=%d", want, got) 142 | } 143 | 144 | want = 1 145 | got = res[0].InstructorID 146 | 147 | if err != nil { 148 | t.Errorf("want=%d; got=%d", want, got) 149 | } 150 | } 151 | 152 | func TestGetCoursesWithInstructorAndAttendee_server(t *testing.T) { 153 | ts := httptest.NewServer(http.HandlerFunc(GetCoursesWithInstructorAndAttendee)) 154 | defer ts.Close() 155 | 156 | resp, err := http.Get(ts.URL + "?instructor=1&attendee=2") 157 | if err != nil { 158 | t.Errorf("get error=%s, wanted nil", err) 159 | } 160 | 161 | body, err := ioutil.ReadAll(resp.Body) 162 | if err != nil { 163 | t.Errorf("readAll err=%s; want nil", err) 164 | } 165 | 166 | var res []Course 167 | err = json.Unmarshal(body, &res) 168 | if err != nil { 169 | t.Errorf("unmarshal err=%s; want nil", err) 170 | } 171 | want := 1 172 | got := len(res) 173 | 174 | if err != nil { 175 | t.Errorf("want=%d; got=%d", want, got) 176 | } 177 | 178 | want = 1 179 | got = res[0].InstructorID 180 | 181 | if err != nil { 182 | t.Errorf("want=%d; got=%d", want, got) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/instructor.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | instructors []Instructor 14 | ) 15 | 16 | func init() { 17 | loadInstructors("./data/instructors.json") 18 | } 19 | 20 | func loadInstructors(path string) { 21 | if err := readContent(path, &instructors); err != nil { 22 | log.Println("Could not read instructors data") 23 | } 24 | } 25 | 26 | func GetAllInstructors(w http.ResponseWriter, r *http.Request) { 27 | w.Header().Add("Content-Type", "application/json") 28 | 29 | query := r.URL.Query() 30 | expertise, ok := query["expertise"] 31 | // the key was found. 32 | if ok { 33 | res := make([]Instructor, 0) 34 | for _, instructor := range instructors { 35 | if contains(instructor.Expertise, expertise) { 36 | res = append(res, instructor) 37 | } 38 | } 39 | 40 | e := json.NewEncoder(w) 41 | e.Encode(res) 42 | return 43 | } 44 | 45 | e := json.NewEncoder(w) 46 | e.Encode(instructors) 47 | } 48 | 49 | func GetInstructorByID(w http.ResponseWriter, r *http.Request) { 50 | w.Header().Add("Content-Type", "application/json") 51 | pathParams := mux.Vars(r) 52 | id := -1 53 | var err error 54 | if val, ok := pathParams["id"]; ok { 55 | id, err = strconv.Atoi(val) 56 | if err != nil { 57 | w.WriteHeader(http.StatusBadRequest) 58 | w.Write([]byte(`{"error": "need a valid id"}`)) 59 | return 60 | } 61 | } 62 | 63 | var data *Instructor 64 | for _, v := range instructors { 65 | if v.ID == id { 66 | data = &v 67 | break 68 | } 69 | } 70 | 71 | if data == nil { 72 | w.WriteHeader(http.StatusNotFound) 73 | w.Write([]byte(`{"error": "not found"}`)) 74 | } 75 | 76 | e := json.NewEncoder(w) 77 | e.Encode(data) 78 | } 79 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/server.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 9 | w.Header().Add("Content-Type", "application/json") 10 | e := json.NewEncoder(w) 11 | e.Encode(s.Routes) 12 | } 13 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/types.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | type Server struct { 4 | Routes []string `json:"routes"` 5 | } 6 | 7 | // User represent one user of our service 8 | type User struct { 9 | ID int `json:"id"` 10 | Name string `json:"name"` 11 | Email string `json:"email"` 12 | Company string `json:"company"` 13 | Interests []string `json:"interests"` 14 | } 15 | 16 | // Instructor type represent a instructor for a course 17 | type Instructor struct { 18 | ID int `json:"id"` 19 | Name string `json:"name"` 20 | Email string `json:"email"` 21 | Company string `json:"company"` 22 | Expertise []string `json:"expertise"` 23 | } 24 | 25 | // Course is course being taught 26 | type Course struct { 27 | ID int `json:"id"` 28 | InstructorID int `json:"instructor_id"` 29 | Name string `json:"name"` 30 | Topics []string `json:"topics"` 31 | Attendees []int `json:"attendees"` 32 | } 33 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | var ( 13 | users []User 14 | ) 15 | 16 | func init() { 17 | loadUsers("./data/users.json") 18 | } 19 | 20 | func loadUsers(path string) { 21 | if err := readContent(path, &users); err != nil { 22 | log.Println("Could not read instructors data") 23 | } 24 | } 25 | 26 | func GetAllUsers(w http.ResponseWriter, r *http.Request) { 27 | w.Header().Add("Content-Type", "application/json") 28 | 29 | query := r.URL.Query() 30 | interests, ok := query["interest"] 31 | // the key was found. 32 | if ok { 33 | res := make([]User, 0) 34 | for _, user := range users { 35 | if contains(user.Interests, interests) { 36 | res = append(res, user) 37 | } 38 | } 39 | 40 | e := json.NewEncoder(w) 41 | e.Encode(res) 42 | return 43 | } 44 | 45 | e := json.NewEncoder(w) 46 | e.Encode(users) 47 | } 48 | 49 | func GetUserByID(w http.ResponseWriter, r *http.Request) { 50 | w.Header().Add("Content-Type", "application/json") 51 | pathParams := mux.Vars(r) 52 | id := -1 53 | var err error 54 | if val, ok := pathParams["id"]; ok { 55 | id, err = strconv.Atoi(val) 56 | if err != nil { 57 | w.WriteHeader(http.StatusBadRequest) 58 | w.Write([]byte(`{"error": "need a valid id"}`)) 59 | return 60 | } 61 | } 62 | 63 | var data *User 64 | for _, v := range users { 65 | if v.ID == id { 66 | data = &v 67 | break 68 | } 69 | } 70 | 71 | if data == nil { 72 | w.WriteHeader(http.StatusNotFound) 73 | w.Write([]byte(`{"error": "not found"}`)) 74 | } 75 | 76 | e := json.NewEncoder(w) 77 | e.Encode(data) 78 | } 79 | -------------------------------------------------------------------------------- /testing-benchmark/internal/handler/utils.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func readContent(filename string, store interface{}) error { 12 | f, err := os.Open(filename) 13 | if err != nil { 14 | return err 15 | } 16 | b, err := ioutil.ReadAll(f) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | return json.Unmarshal(b, store) 22 | } 23 | 24 | func contains(in []string, val []string) bool { 25 | found := 0 26 | 27 | for _, n := range in { 28 | n = strings.ToLower(n) 29 | for _, v := range val { 30 | if n == strings.ToLower(v) { 31 | found++ 32 | break 33 | } 34 | } 35 | } 36 | 37 | return len(val) == found 38 | } 39 | 40 | func containsInt(in []int, val []string) bool { 41 | found := 0 42 | for _, _n := range in { 43 | n := strconv.Itoa(_n) 44 | for _, v := range val { 45 | if n == v { 46 | found++ 47 | break 48 | } 49 | } 50 | } 51 | 52 | return len(val) == found 53 | } 54 | -------------------------------------------------------------------------------- /testing-benchmark/internal/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | ) 11 | 12 | func JWTAuth(next http.Handler) http.Handler { 13 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | authorization := r.Header.Get("Authorization") 15 | // auth token have the structure `bearer ` 16 | // so we split it on the ` ` (space character) 17 | splitToken := strings.Split(authorization, " ") 18 | // if we end up with a array of size 2 we have the token as the 19 | // 2nd item in the array 20 | if len(splitToken) != 2 { 21 | // we got something different 22 | w.WriteHeader(http.StatusUnauthorized) 23 | w.Write([]byte(`{"error": "not authorized"}`)) 24 | return 25 | } 26 | // second item is our possible token 27 | jwtToken := splitToken[1] 28 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 29 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 30 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 31 | } 32 | return []byte("very-secret"), nil 33 | }) 34 | 35 | if err != nil { 36 | // we got something different 37 | w.WriteHeader(http.StatusUnauthorized) 38 | w.Write([]byte(`{"error": "not authorized"}`)) 39 | return 40 | } 41 | 42 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 43 | ctx := context.WithValue(r.Context(), ContextKey("props"), claims) 44 | next.ServeHTTP(w, r.WithContext(ctx)) 45 | } else { 46 | w.WriteHeader(http.StatusUnauthorized) 47 | w.Write([]byte(`{"error": "not authorized"}`)) 48 | } 49 | 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /testing-benchmark/internal/middleware/types.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | // ContextKey is not primitive type key for context 4 | type ContextKey string 5 | -------------------------------------------------------------------------------- /testing-benchmark/pkg/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/gorilla/handlers" 10 | ) 11 | 12 | func Chain(f http.Handler, 13 | middlewares ...func(next http.Handler) http.Handler) http.Handler { 14 | for _, m := range middlewares { 15 | f = m(f) 16 | } 17 | return f 18 | } 19 | 20 | func Logger(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | log.Println(r.URL) 23 | next.ServeHTTP(w, r) 24 | }) 25 | } 26 | 27 | func MuxLogger(next http.Handler) http.Handler { 28 | return handlers.LoggingHandler(os.Stdout, next) 29 | } 30 | 31 | func Time(next http.Handler) http.Handler { 32 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | start := time.Now() 34 | defer func() { log.Println(r.URL.Path, time.Since(start)) }() 35 | next.ServeHTTP(w, r) 36 | }) 37 | } 38 | --------------------------------------------------------------------------------