Go Echo API Server Development

Create a easy api server primarily based on a minimal configuration begin challenge with the next capabilities

  • db migration by sql-migrate
  • db operation from apps by gorm
  • Enter examine by go-playground/validator
  • Switching of configuration information for every manufacturing and growth surroundings
  • Consumer authentication middleware

https://github.com/nrikiji/go-echo-sample

Additionally, assume Firebase Authentication for person authentication and MySQL for database



What we make

Two APIs, one to retrieve a listing of weblog posts and the opposite to replace posted posts. The API to record articles will be accessed by anybody, and the API to replace articles can solely be accessed by the one who posted the article.



Put together



Setup

Clone the bottom challenge

$ git clone https://github.com/nrikiji/go-echo-sample
Enter fullscreen mode

Exit fullscreen mode

Edit database connection data to match your surroundings
config.yml

growth:
  dialect: mysql
  datasource: root:@tcp(localhost:3306)/go-echo-example?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true
  dir: migrations
  desk: migrations
...  
Enter fullscreen mode

Exit fullscreen mode

posts desk creation

$ sql-migrate -config config.yml create_posts

$ vi migrations/xxxxxxx-create_posts.sql
-- +migrate Up
CREATE TABLE `posts` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL,
  `title` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `physique` textual content COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  FOREIGN KEY (`user_id`) REFERENCES `customers` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

$ sql-migrate up -env growth -config config.yml 
$ sql-migrate up -env check -config config.yml
Enter fullscreen mode

Exit fullscreen mode

Additionally, the migration file for the customers desk is included within the base challenge (easy desk with solely id, title and firebase_uid)



Register dummy knowledge

Create a person with e-mail tackle + password authentication from the firebase console and acquire an API key for the net (Internet API key in Undertaking Settings > Basic).

Additionally, add the non-public key for utilizing Firebase Admin SDK (Firebase Admin SDK in Undertaking Settings > Service Account) to the basis of the challenge. (On this case, the file title is firebase_secret_key.json.

Get hold of the localId (Firebase person ID) and idToken of the registered person from the API. localId is about in customers.firebase_uid and idToken is about within the http header when requesting the API.

This time, request on to firebase login API to get idToken and localId

$ curl 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=APIキー' 
-h 'Content material-Sort: utility/json' 
-d '{"e-mail": "foo@instance.com", "password": "password", "returnSecureToken":true}' | jq

{
  "localId": "xxxxxxxxxxxxxxx",
  "idToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  }
}
Enter fullscreen mode

Exit fullscreen mode

Register a person in DB with the obtained localId.

insert into customers (id, firebase_uid, title, created_at, updated_at) values
  (1, "xxxxxxxxxxxxxxx", "user1", now(), now());

insert into posts (user_id, title, physique, created_at, updated_at) values
  (1, "title1", "body1", now(), now()), (2, "title2", "body2", now(), now());
Enter fullscreen mode

Exit fullscreen mode

Now we’re prepared for growth



Implement the info manipulation half

Put together a mannequin that represents information retrieved from DB.

mannequin/put up.go

bundle mannequin

sort Put up struct {
    ID uint `gorm: "primaryKey" json: "id"`
    UserID uint `json: "user_id"`
    Consumer Consumer `json: "person"`
    Title string `json: "title"`
    Physique string `json: "physique"`
}
Enter fullscreen mode

Exit fullscreen mode

Use gorm so as to add strategies to the shop to retrieve from and replace the DB. Since we have now a UserStore within the base challenge, we add the AllPosts and UpdatePost strategies to it this time

retailer/put up.go

bundle retailer

import (
    "errors".
    "go-echo-starter/mannequin"

    "gorm.io/gorm"
)

func (us *UserStore) AllPosts() ([]mannequin.Put up, error) {
    var p []mannequin.Put up
    err := us.db.Preload("Consumer").Discover(&p).Error
    if err ! = nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return p, nil
        }
        return nil, err
    }
    return p, nil
}

func (us *UserStore) UpdatePost(put up *mannequin.Put up) error {
    return us.db.Mannequin(put up).Updates(put up).Error
}
Enter fullscreen mode

Exit fullscreen mode



Implementing the acquisition API

Implement the half that acquires the mannequin from the shop and returns the response in json when requested.



Implementation

handler/put up.go

bundle handler

Import (
    "go-echo-starter/mannequin" "web/http"
    "web/http"

    "github.com/labstack/echo/v4"
)

sort postsResponse struct { (sort postsResponse struct)
    posts []mannequin.Put up `json: "posts"`
}

func (h *Handler) getPosts(c echo.Context) error {.
    posts, err := h.userStore.AllPosts()
    if err ! = nil {
        return err

    return c.JSON(http.StatusOK, postsResponse{Posts: posts}))
}
Enter fullscreen mode

Exit fullscreen mode

Name the handler when a GET request is made with a path named /posts in a route

handler/routes.go

bundle handler

Import (
    "go-echo-starter/middleware"

    "github.com/labstack/echo/v4"
)

func (h *Handler) Register(api *echo.Group){.
    ...
    api.GET("/posts", h.getPosts)
}
Enter fullscreen mode

Exit fullscreen mode



Test operation

$ go run server.go
...

$ curl http://localhost:8000/api/posts | jq
{
  "posts": [
    {
      "id": 1,
      "user_id": 1,
      "user": {
        "id": 1,
        "name": "user1",
      },
      "title": "title1",
      "body": "body1",
    },
    }, "title": "title1", "body": "body1", }
}
Enter fullscreen mode

Exit fullscreen mode



write test

Prepare two test data with fixtures

fixtures/posts.yml

- id: 1
  user_id: 1
  title: "Title1"
  body: "Body1"

- id: 2
  user_id: 2
  title: "Title2"
  body: "Body2"
Enter fullscreen mode

Exit fullscreen mode

Write tests for the handler. Here we test that there is no error, and that the number of items matches.

handler/post_test.go

package handler

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/labstack/echo/v4"
    "github.com/stretchr/testify/assert"
)

func TestGetPosts(t *testing.T) {
    setup()
    req := httptest.NewRequest(echo.GET, "/api/posts", nil)
    rec := httptest.NewRecorder()

    c := e.NewContext(req, rec)
    assert.NoError(t, h.getPosts(c))

    if assert.Equal(t, http.StatusOK, rec.Code) {
        var res postsResponse
        err := json.Unmarshal(rec.Body.Bytes(), &res)
        assert.NoError(t, err)
        assert.Equal(t, 2, len(res.Posts))
    }
}
Enter fullscreen mode

Exit fullscreen mode

Run a test

$ cd handler
$ go test -run TestGetPosts
...
ok go-echo-starter/handler 1.380s
Enter fullscreen mode

Exit fullscreen mode



Implement update API

Apply auth middleware for authentication to prevent others from updating their posts. What this middleware does is to get the firebase user id from the firebase idToken set in the http header Authorization: Bearer xxxxx, search the users table using the UID as a key, and set the result in The result is set in context.

In the handler, if the user can be retrieved from the context, authentication succeeds; if not, authentication fails.

user := context.Get("user")
if user == nil {
    // authentication fails
} else {
    // authentication succeeded
}
Enter fullscreen mode

Exit fullscreen mode



Implementation

handler/post.go

type postResponse struct { { type postResponse struct
    post model.Post `json: "post"`.
}

type updatePostRequest struct { { { type updatePostRequest
    title string `json: "title" validate: "required"`
    body string `json: "body" validate: "required"`
}

func (h *Handler) updatePost(c echo.Context) error { // get user information
    // get user information
    u := c.Get("user")

    if u == nil {
        return c.JSON(http.StatusForbidden, nil)


    user := u.(*model.User)

    // get article
    id, _ := strconv.Atoi(c.Param("id"))
    post, err := h.userStore.FindPostByID(id)

    if err ! = nil { {.
        return c.JSON(http.StatusInternalServerError, nil)
    } else if post == nil {
        return c.JSON(http.StatusNotFound, nil)
    }

    // if it is someone else's post, consider it as unauthorized access
    if post.UserID ! = user.ID { { { if post.UserID ! = user.ID { { { if post.UserID !
        return c.JSON(http.StatusForbidden, nil)
    }

    params := &updatePostRequest{}
    if err := c.Bind(params); err ! = nil { {.
        return c.JSON(http.StatusInternalServerError, nil)
    }

    // Validation
    if err := c.Validate(params); err ! = nil { { if err := c.Validate(params); err !
        return c.JSON(
            http.StatusBadRequest,
            ae.NewValidationError(err, ae.ValidationMessages{
                "Title": {"required": "Please enter a title"},
                "Body": {"required": "Please enter a body"},
            }),
        )
    }

    // Update data
    post.Title = params.
    Post.Body = params.Body

    if err := h.userStore.UpdatePost(post); err ! = nil { .
        return c.JSON(http.StatusInternalServerError, nil)
    }

    return c.JSON(http.StatusOK, postResponse{Post: *post}))
}
Enter fullscreen mode

Exit fullscreen mode

Validation can be done using go-playground/validator’s (https://github.com/go-playground/validator/blob/master/translations/ja/ja.go) functionality, which allows you to display default multilingual display of error messages. However, this app does not use it, but instead defines a map keyed by field name and validation rule name, and uses display fixed messages.

if err := c.Validate(params); err ! = nil {
    return c.JSON(
        http.StatusBadRequest,
        ae.NewValidationError(err, ae.ValidationMessages{
            "Title": {"required": "required Title."},
            "Body": {"required": "required Body"},
        }),
    )
}
Enter fullscreen mode

Exit fullscreen mode

Next, call the handler you created when a PATCH request is made in routes with the path /posts

handler/routes.go

func (h *Handler) Register(api *echo.Group) {
    Auth := middleware.AuthMiddleware(h.authStore, h.userStore)
    ...
    api.PATCH("/posts/:id", h.updatePost, auth)
}
Enter fullscreen mode

Exit fullscreen mode



Confirmation of operation

Put the firebase idToken obtained above in the http header and check the operation.

$ go run server.go
...

$ curl -X PATCH -H "Content-Type: application/json" frz
-H "Authorization: Bearer xxxxxxxxxxxxxx" $ curl
-d '{"title": "NewTitle", "body": "NewBody1"}' 
http://localhost:8000/api/posts/1 | jq

{
  "post": {
    "id": 1,
    "title": "NewTitle",
    "body": "NewBody1",
    }
  }
}
Enter fullscreen mode

Exit fullscreen mode

Checking for errors when trying to update someone else’s article

$ curl -X PATCH -H "Content-Type: application/json" 
-H "Authorization: Bearer xxxxxxxxxxxxxx" }
-d '{"title": "NewTitle", "body": "NewBody1"}' 
http://localhost:8000/api/posts/2 -v

...
HTTP/1.1 403 Forbidden
...
Enter fullscreen mode

Exit fullscreen mode



Writing Tests

Handler tests that you can update your own articles, but not others’.

Update your own article

handler/post_test.go

func TestUpdatePostSuccess(t *testing.T) {
    setup()

    reqJSON := `{"title":"NewTitle", "body":"NewBody"}`
    authMiddleware := middleware.AuthMiddleware(h.authStore, h.userStore)

    req := httptest.NewRequest(echo.PATCH, "/api/posts/:id", strings.NewReader(reqJSON))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    req.Header.Set(echo.HeaderAuthorization, "Bearer: ValidToken1")

    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)
    c.SetPath("/api/posts/:id")
    c.SetParamNames("id")
    c.SetParamValues("1")

    err := authMiddleware(func(c echo.Context) error {
        return h.updatePost(c)
    })(c)
    assert.NoError(t, err)

    if assert.Equal(t, http.StatusOK, rec.Code) {
        var res postResponse
        err := json.Unmarshal(rec.Body.Bytes(), &res)
        assert.NoError(t, err)
        assert.Equal(t, "NewTitle", res.Post.Title)
        assert.Equal(t, "NewBody", res.Post.Body)
    }
}
Enter fullscreen mode

Exit fullscreen mode

test returns a fixed user id by idToken for the conversion of idToken to Firebase user id, which is done by the authentication middleware. Use the mock method prepared in base project.

func (f *fakeAuthClient) VerifyIDToken(context context.Context, token string) (*auth.Token, error) {
    var uid string
    if token == "ValidToken" {
        uid = "ValidUID"
        return &auth.Token{UID: uid}, nil
    } else if token == "ValidToken1" {
        uid = "ValidUID1"
        return &auth.Token{UID: uid}, nil
    } else {
        return nil, errors.New("Invalid Token")
    }
}
Enter fullscreen mode

Exit fullscreen mode

Trying to update someone else’s article.

handler/post_test.go

func TestUpdatePostForbidden(t *testing.T) {
    setup()

    reqJSON := `{"title":"NewTitle", "body":"NewBody"}`
    authMiddleware := middleware.AuthMiddleware(h.authStore, h.userStore)

    req := httptest.NewRequest(echo.PATCH, "/api/posts/:id", strings.NewReader(reqJSON))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    req.Header.Set(echo.HeaderAuthorization, "Bearer: ValidToken1")

    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)
    c.SetPath("/api/posts/:id")
    c.SetParamNames("id")
    c.SetParamValues("2")

    err := authMiddleware(func(c echo.Context) error {
        return h.updatePost(c)
    })(c)
    assert.NoError(t, err)
    assert.Equal(t, http.StatusForbidden, rec.Code)
}
Enter fullscreen mode

Exit fullscreen mode

test run

$ go test -run TestUpdatePostSuccess
・・・
ok      go-echo-starter/handler 1.380s

$ go test -run TestUpdatePostForbidden
・・・
ok      go-echo-starter/handler 1.380s
Enter fullscreen mode

Exit fullscreen mode



Conclusion

Sample we made this time
https://github.com/nrikiji/go-echo-sample/tree/blog-example

Add a Comment

Your email address will not be published. Required fields are marked *