Developing a RESTful API with Go
Tags: go, webdev, programming, opensource
Published: 2022-05-15 14:29:19

## When to use RESTful API

For most startups, we should focus more on delivering the products in the early stage of business. The monolithic services have the advantages of simple architecture, easy deployment, and better development productivity, which can help us achieve the product requirements quickly. While we use monolithic services to deliver products quickly, we also need to reserve the possibility for business incresement, so we usually split different business modules clearly in monolithic services.

## Shopping mall monolithic service architecture

We take the mall as an example to build a monolithic service. The mall service is generally relatively complex and consists of multiple modules, the more important modules include account, product and order modules, etc. Each module will have its own independent business logic, and each module will also depend on some others. For example, the order module and the product module will depend on the account module. In the monolithic application this kind of dependency is usually accomplished by method calls between modules. Monolithic services generally share storage resources, such as `MySQL` and `Redis`.

The overall architecture of monolithic services is relatively simple, which is also the advantage of monolithic services. Customer requests are parsed through `DNS` and forwarded to the mall's backend services through `Nginx`. Mall services are deployed on cloud hosts. In order to achieve greater throughput and high availability, the service will generally deployed with multiple copies. This simple architecture can carry high throughput if well optimized. 

![](https://oscimg.oschina.net/oscnet/up-7989674424cc1d3600ecc6f3ff056bca399.png)

For example, a request for order details interface `/order/detail` is routed to the order module, which relies on the account module and the product module to compose the complete order details back to the user, and multiple modules in a single service generally share the database and cache.

![](https://oscimg.oschina.net/oscnet/up-d208b1164d2e03149913f8d310d8b694710.png)

## Monolithic Service

The next section describes how to quickly implement a mall monolithic service based on `go-zero`. Devs who have used `go-zero` know that we provide an `API` format file to describe the `Restful API`, and then we can generate the corresponding code by `goctl` with one command, we just need to fill in the corresponding business logic in the `logic` files. The mall service contains several modules, and in order to make the modules independent from each other, different modules are defined by separate `API`s, but all the `API`s are defined for the same `service (mall-api)`.

Create `user.api`, `order.api`, `product.api` and `mall.api` in the `api` directory, where `mall.api` is the aggregated `api` file. Other `api` files are imported via `import` directives.

```
api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api
```

### Mall API Definition

`mall.api` is defined as follows, where `syntax = "v1"` means that this is the `v1` syntax of `zero-api`.

```
syntax = "v1"

import "user.api"
import "order.api"
import "product.api"
```

### Account module API definition

- View user details  
- Get all orders for a user

`user.api` is defined as follows.

```go
syntax = "v1"

type (
    UserRequest {
        ID int64 `path:"id"`
    }

    UserReply {
        ID      int64   `json:"id"`
        Name    string  `json:"name"`
        Balance float64 `json:"balance"`
    }

    UserOrdersRequest {
        ID int64 `path:"id"`
    }

    UserOrdersReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }
)

service mall-api {
    @handler UserHandler
    get /user/:id (UserRequest) returns (UserReply)

    @handler UserOrdersHandler
    get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}
```

### Order module API definition

- Get order details
- Generate orders

`order.api` is defined as follows.

```go
syntax = "v1"

type (
    OrderRequest {
        ID string `path:"id"`
    }

    OrderReply {
        ID       string `json:"id"`
        State    uint32 `json:"state"`
        CreateAt string `json:"create_at"`
    }

    OrderCreateRequest {
        ProductID int64 `json:"product_id"`
    }

    OrderCreateReply {
        Code int `json:"code"`
    }
)

service mall-api {
    @handler OrderHandler
    get /order/:id (OrderRequest) returns (OrderReply)

    @handler OrderCreateHandler
    post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}
```

### Product module API definition

- View product details

`product.api` is defined as follows.

```go
syntax = "v1"

type ProductRequest {
    ID int64 `path:"id"`
}

type ProductReply {
    ID    int64   `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Count int64   `json:"count"`
}

service mall-api {
    @handler ProductHandler
    get /product/:id (ProductRequest) returns (ProductReply)
}
```

### Generating the monolithic service

With the `API` already defined, generating a service with the `API` becomes very simple, we use `goctl` to generate the monolithic service code.

```shell
$ goctl api go -api api/mall.api -dir .
```

The generated code is structured as follows.

```go
.
├── api
│ ├── mall.api
│ ├── order.api
│ ├── product.api
│ └── user.api
├── etc
│ └── mall-api.yaml
├─ internal
│ ├── config
│ │ └── config.go
│ ├── handler
│ │ ├── ordercreatehandler.go
│ │ ├── orderhandler.go
│ │ ├── producthandler.go
│ │ ├── routes.go
│ │ ├── userhandler.go
│ │ └─ userordershandler.go
│ ├─ logic
│ │ ├─ ordercreatelogic.go
│ │ ├── orderlogic.go
│ │ ├── productlogic.go
│ │ ├── userlogic.go
│ │ └── userorderslogic.go
│ ├── svc
│ │ └── servicecontext.go
│ └── types
│ └── types.go
└── mall.go
```

Let's explain the generated files.

- `api`: holds the `API` description file
- `etc`: used to define the project configuration, all configuration items can be written in `mall-api.yaml`
- `internal/config`: the configuration definition of the service
- `internal/handler`: the implementation of the `handler` corresponding to the routes defined in the `API` file
- `internal/logic`: used to put the business logic corresponding to each route, the reason for the distinction between `handler` and `logic` is to make the business processing part as less dependent as possible, to separate `HTTP requests` from the logic processing code, and to facilitate the subsequent splitting into `RPC service`
- `internal/svc`: used to define the dependencies of the business logic processing, we can create the dependent resources in the `main` function and pass them to `handler` and `logic` via `ServiceContext`
- `internal/types`: defines the `API` request and response data structures
- `mall.go`: the file where the `main` function is located, with the same name as the `service` in the `API` definition, minus the `-api` suffix

The generated service can be run without any modification:

```shell
$ go run mall.go
Starting server at 0.0.0.0:8888...
```

### Implementing the business logic

Next, let's implement the business logic. The logic will be simple for demonstration purposes, not real business logic.

First, let's implement the logic of getting all orders for users. Since there is no order-related information in the user module, we need to rely on the order module to query the orders of users, so we add a dependency on `OrderLogic` in `UserOrdersLogic`.

```go
type UserOrdersLogic struct {
    logx.Logger
    ctx        context.Context
    svcCtx     *svc.ServiceContext
    orderLogic *OrderLogic
}

func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
    return &UserOrdersLogic{
        Logger:     logx.WithContext(ctx),
        ctx:        ctx,
        svcCtx:     svcCtx,
        orderLogic: NewOrderLogic(ctx, svcCtx),
    }
}
```

Implement a method in `OrderLogic` to query all orders based on `user id`

```go
func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
    if uid == 123 {
        // It should actually be queried from database or cache
        return []*types.OrderReply{
            {
                ID:       "236802838635",
                State:    1,
                CreateAt: "2022-5-12 22:59:59",
            },
            {
                ID:       "236802838636",
                State:    1,
                CreateAt: "2022-5-10 20:59:59",
            },
        }, nil
    }

    return nil, nil
}
```

Call the `ordersByUser` method in the `UserOrders` method of `UserOrdersLogic`.

```go
func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
    orders, err := l.orderLogic.ordersByUser(req.ID)
    if err ! = nil {
        return nil, err
    }

    return &types.UserOrdersReply{
        Orders: orders,
    }, nil
}
```

At this point we restart the `mall-api` service and request all the user's orders in the browser.

```
http://localhost:8888/user/123/orders
```

The return result is as follows, as we expected

```json
{
    "orders": [
        {
            "id": "236802838635",
            "state": 1,
            "create_at": "2022-5-12 22:59:59"
        },
        {
            "id": "236802838636",
            "state": 1,
            "create_at": "2022-5-10 20:59:59"
        }
    ]
}
```

Next we'll implement the logic for creating an order. To create an order we first need to see if the item in stock is enough, so we need to rely on the item module in the order module.

```go
type OrderCreateLogic struct {
    logx.Logger
    ctx          context.Context
    svcCtx       *svc.ServiceContext
    productLogic *ProductLogic
    productLogic *ProductLogic
}

func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
    return &OrderCreateLogic{
        Logger:       logx.WithContext(ctx),
        ctx:          ctx,
        svcCtx:       svcCtx,
        productLogic: NewProductLogic(ctx, svcCtx),
    }
}
```

The logic for creating an order is as follows.

```go
const (
    success = 0
    failure = -1
)

func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
    product, err := l.productLogic.productByID(req.ProductID)
    if err ! = nil {
        return nil, err
    }

    if product.Count > 0 {
        return &types.OrderCreateReply{Code: success}, nil
    }

    return &types.OrderCreateReply{Code: failure}, nil
}
```

The logic of the dependent product module is as follows.

```go
func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
    return l.productByID(req.ID)
}

func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
    return &types.ProductReply{
        ID: id,
        Name: "apple watch 3",
        Price: 3333.33,
        Count: 99,
    }, nil
}
```

The above shows that using `go-zero` to develop a monolithic service is very simple, which helps us to develop quickly. And we also separated modules, which also provides the possibility of changing to microservices later.

## Summary

The above example shows that it is very simple to use `go-zero` to develop monolithic services. You only need to define the `api` file, and then the `goctl` tool can automatically generate the project code. We only need to fill in the business logic code in the logic package. In this article we just demonstrated how to quickly develop monolithic services based on `go-zero`, which does not involve databases. In fact, `goctl` can also generate `CRUD` and `cache` code with one command.

And for different business scenarios, customization can also be achieved through customizing templates. And customized templates can be shared within the team through remote `git` repositories, which can be very efficient for team collaboration.

## Project address

[https://github.com/zeromicro/go-zero](https://github.com/zeromicro/go-zero)

Welcome to use `go-zero` and **star** to support us!