Microservice example application with Docker Swarm, Golang, gRPC, GraphQL, TypeScript and React - Part I
This post will cover creation of folder structure, gateway and auth services’ scaffold.
What is it?
Hello, in this blog post I will create an example microservices structure using a graphql gateway API and a client written in TypeScript and React.
Before start coding, let us look into drawing for basic structure:
This project will simply login users with mock authentication data and allow them to add products to carts and create orders. I won’t be using and kind of data storage for now because of I want to keep it simple.
Project components
Auth service
This service will handle login requests and respond with generated mock login info like first name, avatar.
Product service
This service will serve product list.
Cart service
This service will handle:
- add to cart,
- remove from cart,
- flush cart (when order is completed)
Order service
This service will handle order creations.
API Gateway
This will be façade of our project. Our React application will talk with this gateway via GraphQL.
React Application
This will handle all of operations and serve us graphical interface for our project.
Let us commence!
Creating gateway service
I will name this project “fake_store”. You can find it on GitHub
First create folder with
mkdir fake_store
Initialize Go module and touch docker-compose file.
go mod init github.com/yigitsadic/fake_store
touch docker-compose.yml
I want to start with GraphQL API Gateway
mkdir gateway && cd gateway
I will use github.com/99designs/gqlgen for handling GraphQL server in Go.
go get github.com/99designs/gqlgen
go run github.com/99designs/gqlgen init .
This will generate some boilerplate for our project. I took it from it’s docs.
├── gqlgen.yml - The gqlgen config file, knobs for controlling the generated code.
├── graph
│ ├── generated - A package that only contains the generated runtime
│ │ └── generated.go
│ ├── model - A package for all your graph models, generated or otherwise
│ │ └── models_gen.go
│ ├── resolver.go - The root graph resolver type. This file wont get regenerated
│ ├── schema.graphqls - Some schema. You can split the schema into as many graphql files as you like
│ └── schema.resolvers.go - the resolver implementation for schema.graphql
└── server.go - The entry point to your app. Customize it however you see fit
At this point, you can use go run ./server.go
to fire up GraphQL server. gqlgen works with schema-first principle.
For code generation from GraphQL schema we need to run go run github.com/99designs/gqlgen generate
. But there is a shorthand for it.
Add this line to top of your gateway/graph/resolver.go
file (this method recommended in gqlgen docs).
//go:generate go run github.com/99designs/gqlgen
package graph
type Resolver struct {}
with this piece of code we can run go generate ./...
in our command line and generate Go code from GraphQL schema.
gqlgen generates standard library compatible GraphQL server. You can use standard library http package or gorilla or chi routers. Personally I like to use chi router.
To install chi you can go get -u github.com/go-chi/chi/v5
and for CORS go get -u github.com/rs/cors
The code generated with gqlgen init is using standard http package. Now, we’ll connect chi router with gqlgen GraphQL server.
We will delete generated server.go
file and create new file under /cmd
folder:
// gateway/cmd/main.go
package main
import (
"log"
"net/http"
"os"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/rs/cors"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3035"
}
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
r := chi.NewRouter()
r.Use(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowCredentials: true,
Debug: true,
}).Handler)
r.Use(middleware.Heartbeat("/readiness")) // for healthcheck
r.Handle("/", playground.Handler("GraphQL playground", "/query"))
r.Handle("/query", srv)
log.Printf("Server is up and running on port %s\n", port)
log.Fatal(http.ListenAndServe(":"+port, r))
}
Now for testing purposes we can alter our GraphQL schema with hello world message.
Edit graph/schema.graphqls file like below:
type Query {
sayHello: String!
}
type LoginResponse {
id: ID!
avatar: String!
fullName: String!
token: String!
}
type Mutation {
login: LoginResponse!
}
Remove graph/schema.resolvers.go
file content and run go generate ./...
for code generation. This will update files below:
- graph/generated/generated.go
- graph/models/models_gen.go
- graph/schema.resolvers.go
We’ll be working with schema.resolvers.go
file. Update your schema resolver file like this:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"github.com/yigitsadic/fake_store/gateway/graph/generated"
"github.com/yigitsadic/fake_store/gateway/graph/model"
)
func (r *mutationResolver) Login(ctx context.Context) (*model.LoginResponse, error) {
res := model.LoginResponse{
ID: "21b00554672245329aa05a4596ec09c4",
Avatar: "https://avatars.dicebear.com/api/human/b9e73b73a19d4807b7fc518b0feeca24.svg",
FullName: "Drew Schmidt",
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdmF0YXIiOiJodHRwczovL2F2YXRhcnMuZGljZWJlYXIuY29tL2FwaS9odW1hbi9iOWU3M2I3M2ExOWQ0ODA3YjdmYzUxOGIwZmVlY2EyNC5zdmciLCJmdWxsTmFtZSI6IkRyZXcgU2NobWlkdCIsImV4cCI6MTY2MjczNDY5NzA0NjM4NzIwMCwianRpIjoiMjFiMDA1NTQ2NzIyNDUzMjlhYTA1YTQ1OTZlYzA5YzQiLCJpYXQiOjE2MzExOTg2OTcwNDY0MDUxMDAsImlzcyI6ImZha2Vfc3RvcmVfYXV0aCJ9.v6tYm0y7wD21G-Ec_1PMEmhnEf0WJMqdALzcWBbsX90",
}
return &res, nil
}
func (r *queryResolver) SayHello(ctx context.Context) (string, error) {
return "Hello World", nil
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Now we’re ready to test our changes. In gateway folder run go run ./cmd
and open localhost:3035 at your browser.
First fire up a query:
And moment of truth. Let’s try login mutation:
We have successfully implemented basic GraphQL server in Go!
Let’s move to Auth Service!
Creating auth service
Root of your project folder, create new folder and initialize with proto file.
mkdir auth && cd auth
touch auth.proto
For protobuf you can visit https://developers.google.com/protocol-buffers.
Update auth.proto
file with:
syntax = "proto3";
package auth;
option go_package = "client/client";
message AuthRequest {}
message UserResponse {
string id = 1;
string avatar = 2;
string fullName = 3;
string jwtToken = 4;
}
service AuthService {
rpc LoginUser(AuthRequest) returns (UserResponse) {}
}
For generate gRPC client and server generation we first need to install
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
protoc --go_out=. --go-grpc_out=. auth.proto
It will generate two files which contains models, client and server.
auth
client
client
auth.pb.go
auth_grpc.pb.go
In order to communicate using gRPC we need a server. Create a file named “auth/cmd/main.go”
I will use faker package for random data generation. Install faker with go get -u github.com/bxcodec/faker/v3
For avatars I will use dicebear project. It gives you consistent random pretty avatars.
For generating JWT tokens, we need to install github.com/dgrijalva/jwt-go with go get -u github.com/dgrijalva/jwt-go
Let’s code JWT generation and gRPC server.
Create auth/cmd/jwt_token.go and insert codes below:
package main
import (
"github.com/dgrijalva/jwt-go"
"time"
)
type Claims struct {
Avatar string `json:"avatar"`
FullName string `json:"fullName"`
jwt.StandardClaims
}
func GenerateJWTToken(id, avatar, fullName string) string {
c := Claims{
Avatar: avatar,
FullName: fullName,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().AddDate(1, 0, 0).UnixNano(),
Id: id,
IssuedAt: time.Now().UnixNano(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
ss, _ := token.SignedString([]byte("FAKE_STORE_AUTH"))
return ss
}
And for the auth/cmd/main.go file we’re basicly initializing the most basic gRPC server:
package main
import (
"context"
"fmt"
"github.com/bxcodec/faker/v3"
"github.com/yigitsadic/fake_store/auth/client/client"
"google.golang.org/grpc"
"log"
"net"
)
const DiceBearUrl = "https://avatars.dicebear.com/api/human/%s.svg"
type Server struct {
client.UnimplementedAuthServiceServer
}
func (s *Server) LoginUser(context.Context, *client.AuthRequest) (*client.UserResponse, error) {
resp := client.UserResponse{
Id: faker.UUIDDigit(),
Avatar: fmt.Sprintf(DiceBearUrl, faker.UUIDDigit()),
FullName: faker.FirstName() + " " + faker.LastName(),
}
resp.JwtToken = GenerateJWTToken(resp.Id, resp.Avatar, resp.FullName)
return &resp, nil
}
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 9000))
if err != nil {
log.Fatalf("failed to listen: %v\n", err)
}
grpcServer := grpc.NewServer()
s := Server{}
client.RegisterAuthServiceServer(grpcServer, &s)
log.Println("Started to serve auth grpc")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve due to %s\n", err)
}
}
Our auth package almost ready. But we need to containerize it. Let’s create Dockerfile.
FROM golang:1.17.0-alpine3.13 as compiler
WORKDIR /src/app
COPY go.mod go.sum ./
COPY auth auth
RUN go build -o auth_service ./auth/cmd/
FROM alpine:3.13
WORKDIR /src
COPY --from=compiler /src/app/auth_service /src/app
CMD ["/src/app"]
I created two-stepped dockerfile. At first step we compile our executable go code. At second step we run our executable in alpine image.
At this step our auth service is fullfil it’s goals. It returns fake full-name along with a jwt token. I won’t implement real auth logic and database operations here (sign-up, sign-in, verify password etc.)
Connecting auth service from gateway
Let’s import and use our gRPC client from gateway. That’s the ease of protobuf and gRPC. Let’s return to gateway
folder.
gqlgen package tells us to we can use Resolver
struct as a dependency injection purposes. I extend gateway/graph/resolver.go
like below.
//go:generate go run github.com/99designs/gqlgen
package graph
import "github.com/yigitsadic/fake_store/auth/client/client" // <- I am using client which generated from protobuf file.
type Resolver struct {
AuthClient client.AuthServiceClient // <- AuthClient will be gRPC client to interact with auth service
}
Update gateway/graph/schema.resolvers.go
for gRPC client use like below:
// ...
func (r *mutationResolver) Login(ctx context.Context) (*model.LoginResponse, error) {
// Make request to auth service
result, err := r.AuthClient.LoginUser(ctx, &client.AuthRequest{})
if err != nil {
return nil, err
}
// Convert response into model.LoginResponse struct
res := model.LoginResponse{
ID: result.Id,
Avatar: result.Avatar,
FullName: result.FullName,
Token: result.JwtToken,
}
return &res, nil
}
// ...
From this point we connected resolver and we need to pass auth service client into resolver struct at initialization of GraphQL server.
Add client and connection generation function to main.go
// gateway/cmd/main.go
func acquireAuthConnection() (*grpc.ClientConn, client.AuthServiceClient) {
conn, err := grpc.Dial("auth:9000", grpc.WithInsecure(), grpc.WithBlock()) // auth:9000 We will be using docker-compose.
if err != nil {
log.Fatalln("Unable to acquire auth connection")
}
c := client.NewAuthServiceClient(conn)
return conn, c
}
Let’s update rest of main.go
file.
// package, import etc.
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3035"
}
// acquire connection and pass resolver as dependency injection.
authConnection, authClient := acquireAuthConnection()
defer authConnection.Close()
resolver := graph.Resolver{
AuthClient: authClient,
}
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver}))
// Rest of code
Let’s continue with Dockerfile.
Create Dockerfile with content:
FROM golang:1.17.0-alpine3.13 as compiler
WORKDIR /src/app
COPY go.mod go.sum ./
COPY gateway gateway
# We need this for access auth gRPC client
COPY auth auth
RUN go build -o gateway_service ./gateway/cmd/
FROM alpine:3.13
WORKDIR /src
COPY --from=compiler /src/app/gateway_service /src/app
CMD ["/src/app"]
Continue with docker-compose.yml file
version: "3.3"
services:
gateway:
build:
context: .
dockerfile: ./gateway/Dockerfile
ports:
- "3035:3035"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:3035/readiness" ]
interval: 200s
timeout: 200s
retries: 5
auth:
build:
context: .
dockerfile: ./auth/Dockerfile
The moment of truth. Run docker-compose up
What’s next?
I will cover React app installation on part II
À la prochaine !