05/06/2020 - GO
In this example we are going to implement a RESTful public HTTP API that communicates with our internal TCP server with a HTTP client for storing data. The process is very simple as shown below.
User (HTTP request) -> RESTful HTTP API (HTTP request) -> TCP Server (TCP response) -> RESTful HTTP API (HTTP response) -> User
You can timeout HTTP request at client side in terms of possible improvements.
├── cmd
│ └── client
│ └── main.go
└── internal
├── pkg
│ ├── client
│ │ └── client.go
│ ├── router
│ │ └── router.go
│ └── server
│ └── server.go
└── user
└── create.go
package main
import (
"log"
"time"
"github.com/inanzzz/client/internal/pkg/client"
"github.com/inanzzz/client/internal/pkg/router"
"github.com/inanzzz/client/internal/pkg/server"
)
func main() {
clt := client.New("http://0.0.0.0:9999/api/v1", 3*time.Second)
rtr := router.New()
rtr.RegisterUser(clt)
srv := server.New("0.0.0.0:8888", rtr.Handler)
log.Fatalln(srv.ListenAndServe())
}
package client
import (
"context"
"net/http"
"strings"
"time"
)
type Client struct {
baseURL string
timeout time.Duration
client http.Client
}
func New(baseURL string, timeout time.Duration) Client {
return Client{
baseURL: baseURL,
timeout: timeout,
client: http.Client{},
}
}
func (c Client) Request(ctx context.Context, method, url, body string, headers map[string]string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+url, strings.NewReader(body))
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Add(k, v)
}
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
return res, nil
}
package router
import (
"net/http"
"github.com/inanzzz/client/internal/pkg/client"
"github.com/inanzzz/client/internal/user"
"github.com/julienschmidt/httprouter"
)
type Router struct {
Handler *httprouter.Router
}
func New() *Router {
rtr := httprouter.New()
rtr.RedirectTrailingSlash = false
rtr.RedirectFixedPath = false
return &Router{
Handler: rtr,
}
}
func (r *Router) RegisterUser(clt client.Client) {
r.Handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(clt).Handle)
}
package server
import (
"net/http"
)
func New(adr string, hnd http.Handler) http.Server {
return http.Server{
Addr: adr,
Handler: hnd,
}
}
package user
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"github.com/inanzzz/client/internal/pkg/client"
)
type CreateRequestBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Create struct {
client client.Client
}
func NewCreate(clt client.Client) Create {
return Create{
client: clt,
}
}
// POST /api/v1/users
func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
var req CreateRequestBody
// Map HTTP request to request model
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Printf("unable to decode request: %v", err)
return
}
// Convert request model to client request body
body, err := json.Marshal(req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Printf("unable to marshal request: %v", err)
return
}
// Store request model
res, err := c.client.Request(context.Background(), http.MethodPost, "/users", string(body), nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("unable to store request: %v", err)
return
}
defer res.Body.Close()
// Check if the response code is an expected value
if res.StatusCode != http.StatusOK {
w.WriteHeader(http.StatusInternalServerError)
log.Print("unable to store data")
return
}
// Convert response model to HTTP response
data, err := ioutil.ReadAll(res.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Print("unable to read client response")
return
}
// Respond
w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(data)
}
├── cmd
│ └── server
│ └── main.go
└── internal
├── pkg
│ ├── http
│ │ ├── router.go
│ │ └── server.go
│ └── tcp
│ └── listener.go
└── user
└── create.go
package main
import (
"log"
"github.com/inanzzz/client/internal/pkg/http"
"github.com/inanzzz/client/internal/pkg/tcp"
nethttp "net/http"
)
func main() {
listener, err := tcp.NewListener("0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
rtr := http.NewRouter()
rtr.RegisterUser(listener)
srv := http.NewServer(*rtr)
if err := srv.Serve(listener); err == nethttp.ErrServerClosed {
log.Println("Shutdown")
}
}
package http
import (
"net"
"net/http"
"github.com/inanzzz/client/internal/user"
"github.com/julienschmidt/httprouter"
)
type Router struct {
handler *httprouter.Router
}
func NewRouter() *Router {
rtr := httprouter.New()
rtr.RedirectTrailingSlash = false
rtr.RedirectFixedPath = false
return &Router{
handler: rtr,
}
}
func (r *Router) RegisterUser(tcpListener net.Listener) {
r.handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(tcpListener).Handle)
}
package http
import (
"net/http"
)
func NewServer(router Router) *http.Server {
return &http.Server{
Handler: router.handler,
}
}
package tcp
import (
"net"
)
func NewListener(address string) (net.Listener, error) {
return net.Listen("tcp", address)
}
You actually don't need net.Listener
below.
package user
import (
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/google/uuid"
)
type Create struct {
tcpListener net.Listener
}
func NewCreate(tcpListener net.Listener) Create {
return Create{
tcpListener: tcpListener,
}
}
func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
log.Println(r.Header.Get("X-Request-ID"))
body := fmt.Sprintf(`{"code":%d,"data":{"id":"%s","created_at":"%s"}}`,
http.StatusOK,
uuid.New().String(),
time.Now().UTC().Format(time.RFC3339),
)
w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write([]byte(body))
}
Assuming that your server and client are running.
curl -i -X POST -d '{"username":"username","password":"password"}' "http://0.0.0.0:8888/api/v1/users"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 05 Jun 2020 17:05:22 GMT
Content-Length: 101
{"code":200,"data":{"id":"7af224a1-5f23-476d-a0a3-92278b0169cc","created_at":"2020-06-05T17:05:22Z"}}
1000 HTTP requests have been served within 20/22 seconds which equals to 1 request per 20/22 milliseconds.