Generate API Boilerplate (for Go)
Wouldn't it be cool if you could also generate all the boilerplate code for your API? You are about to do just that using Go.
You will need to have Go installed and in your PATH. Additionally, you need protoc
and protoc-gen-go
installed.
Protocol Buffer Compiler (protoc) Installation
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Add Go API code generators
Update apex.yaml
with the contents below.
spec: apex.axdl
config:
package: urlshortener
module: github.com/apexlang/getting-started
logger:
import: github.com/go-logr/logr
interface: logr.Logger
generates:
openapi.yaml:
module: 'https://deno.land/x/apex_codegen@v0.1.9/openapiv3/mod.ts'
visitorClass: OpenAPIV3Visitor
proto/service.proto:
module: 'https://deno.land/x/apex_codegen@v0.1.9/proto/mod.ts'
visitorClass: ProtoVisitor
config:
options:
go_package: 'github.com/myorg/myapps/pkg/urlshortener'
runAfter:
- command: |
protoc
--go_out=.
--go_opt=paths=source_relative
--go-grpc_out=.
--go-grpc_opt=paths=source_relative
proto/service.proto
pkg/urlshortener/interfaces.go:
module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: InterfacesVisitor
pkg/urlshortener/api.go:
module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: GoVisitor
config:
protoPackage: github.com/apexlang/getting-started/proto
append:
- module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: FiberVisitor
- module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: GRPCVisitor
pkg/urlshortener/services.go:
ifNotExists: true
module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: ScaffoldVisitor
config:
types:
- service
pkg/urlshortener/repositories.go:
ifNotExists: true
module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: ScaffoldVisitor
config:
names:
- Repository
cmd/main.go:
ifNotExists: false # Change to true to control dependency injection
module: 'https://deno.land/x/apex_codegen@v0.1.9/go/mod.ts'
visitorClass: MainVisitor
Now we are using some more options:
config
- Configuration object that is available to code generation classes. Can be set globally and overridden for each file.ifNotExists
- When true, only generates this file if it does not exist.runAfter
- Runs commands after code generation. Can be used to call other tools such asprotoc
.
Run these commands to regenerate the code, initialize the go module, and download the necessary Go dependencies.
apex generate apex.yaml
go mod init github.com/apexlang/getting-started
go mod tidy
Implement the Service Logic
At this point, you have a fully stubbed out URL Shortener service with all the boilerplate code generated. You only need to implement the core logic. To complete the project, update services.go
and repositories.go
under pkg/urlshortener
with the contents below.
- pkg/urlshortener/services.go
- pkg/urlshortener/repositories.go
package urlshortener
import (
"context"
"errors"
"math/rand"
"time"
"github.com/apexlang/api-go/errorz"
"github.com/apexlang/api-go/transport/httpresponse"
"github.com/go-logr/logr"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var (
randNum = rand.New(rand.NewSource(time.Now().UnixNano()))
)
type ShortenerImpl struct {
log logr.Logger
repository Repository
}
func NewShortener(log logr.Logger, repository Repository) *ShortenerImpl {
return &ShortenerImpl{
log: log,
repository: repository,
}
}
func (s *ShortenerImpl) Shorten(ctx context.Context, url string) (*URL, error) {
urlResp, err := s.repository.LoadByURL(ctx, url)
if err != nil {
var e *errorz.Error
if !(errors.As(err, &e) && e.Code == errorz.NotFound) {
return nil, err
}
} else if urlResp != nil {
// Return already stored URL
return urlResp, nil
}
newURL := URL{
ID: generateID(8),
URL: url,
}
if err = s.repository.StoreURL(ctx, &newURL); err != nil {
return nil, err
}
return &newURL, nil
}
func (s *ShortenerImpl) Lookup(ctx context.Context, id string) (*URL, error) {
url, err := s.repository.LoadByID(ctx, id)
if err != nil {
return nil, err
}
resp := httpresponse.FromContext(ctx)
if resp != nil {
// HTTP specific logic to perform browser redirect.
resp.Header.Add("Location", url.URL)
resp.Status = 302 // StatusFound
}
return url, nil
}
func generateID(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[randNum.Intn(len(letterBytes))]
}
return string(b)
}
package urlshortener
import (
"context"
"sync"
"github.com/apexlang/api-go/errorz"
"github.com/go-logr/logr"
)
type RepositoryImpl struct {
log logr.Logger
mu sync.RWMutex
byID map[string]string
byURL map[string]string
}
func NewRepository(log logr.Logger) *RepositoryImpl {
return &RepositoryImpl{
log: log,
byID: make(map[string]string),
byURL: make(map[string]string),
}
}
func (s *RepositoryImpl) LoadByID(ctx context.Context, id string) (*URL, error) {
s.mu.RLock()
url, ok := s.byID[id]
s.mu.RUnlock()
if !ok {
return nil, errorz.Newf(errorz.NotFound, "URL for ID %s not found", id)
}
return &URL{
ID: id,
URL: url,
}, nil
}
func (s *RepositoryImpl) LoadByURL(ctx context.Context, url string) (*URL, error) {
s.mu.RLock()
id, ok := s.byURL[url]
s.mu.RUnlock()
if !ok {
return nil, errorz.Newf(errorz.NotFound, "URL %s not found", url)
}
return &URL{
ID: id,
URL: url,
}, nil
}
func (s *RepositoryImpl) StoreURL(ctx context.Context, url *URL) error {
s.mu.Lock()
defer s.mu.Unlock()
s.byID[url.ID] = url.URL
s.byURL[url.URL] = url.ID
return nil
}
Run and Test the Service
Finally, run the service and test with your favorite API testing tool, like Postman or Rest Client for VS Code.
go run cmd/main.go