David's Blog

How I made a book api in golang

By David Li on Fri, 10 January 2024

The Google Books API is a web-based API that allows developers to access information about books and perform operations on books such as searching, retrieving, and updating book details. With the Google Books API, developers can build applications that allow users to search the Google Books catalog and retrieve information about books, including author, title, publication date, description, and cover art. They can also use the API to access reviews, ratings, and other metadata about books. The API supports various formats, including plain text, HTML, and PDF, and returns results in both JSON and XML formats.

Using OpenAPI to generate server-side code can be helpful in several ways:

  1. Speed and efficiency: Generating code from an OpenAPI specification can save a significant amount of time compared to manually writing the code from scratch. This can speed up the development process and make it more efficient.
  2. Consistency and accuracy: When generating code, the tools can ensure that the code is consistent and conforms to the OpenAPI specification. This reduces the likelihood of introducing errors or inconsistencies into the codebase, resulting in more accurate and reliable code.
  3. Reusability: OpenAPI allows developers to define their APIs in a standard, language-agnostic format, which can be reused across multiple projects and teams. By using OpenAPI to generate server-side code, developers can take advantage of this reusability and reduce the amount of time and effort required to build and maintain APIs.
  4. Increased productivity: By automating certain aspects of the code generation process, developers can focus on more important tasks, such as designing and implementing new features. This can lead to increased productivity and a more streamlined development process.

Overall, using OpenAPI to generate server-side code can help to reduce the amount of manual effort required to build APIs, increase accuracy and consistency, and make the development process more efficient and productive.

 /*
 * BookApi API (beta ver.)
 *
 *  # Authentication simple no api access, no access token, just a toy project
 *
 * API version: 1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package openapi

import (
	"context"
	"net/http"
)



// BookApiRouter defines the required methods for binding the api requests to a responses for the BookApi
// The BookApiRouter implementation should parse necessary information from the http request,
// pass the data to a BookApiServicer to perform the required actions, then write the service results to the http response.
type BookApiRouter interface { 
	BookGetBook(http.ResponseWriter, *http.Request)
}


// BookApiServicer defines the api actions for the BookApi service
// This interface intended to stay up to date with the openapi yaml used to generate it,
// while the service implementation can be ignored with the .openapi-generator-ignore file
// and updated with the logic required for the API.
type BookApiServicer interface { 
	BookGetBook(context.Context, string, string, string, int32, int32, string, string) (ImplResponse, error)
}
 

This code defines two interfaces, BookApiRouter and BookApiServicer, for a beta version of a simple book API.

The BookApiRouter interface defines the method BookGetBook, which is used to bind the API requests to responses for the Book API. This method is expected to parse information from the incoming HTTP request, pass the data to a BookApiServicer to perform the required actions, and then write the service results to the HTTP response.

The BookApiServicer interface defines the API actions for the Book API service. The interface is intended to stay up-to-date with the OpenAPI specification used to generate it, while the service implementation can be updated with the necessary logic for the API. The BookGetBook method takes in several parameters, such as book title, author, and publication date, and returns an ImplResponse object along with an error.

It looks like this code was generated using the OpenAPI Generator tool, which is designed to automate the process of generating code from OpenAPI specifications. The generated code provides a starting point for developers to implement the logic for their API.

 /*
 * BookApi API (beta ver.)
 *
 *  # Authentication simple no api access, no access token, just a toy project
 *
 * API version: 1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package openapi

import (
	// "encoding/json"
	"net/http"
	"strings"
	// "github.com/gorilla/mux"
)

// BookApiController binds http requests to an api service and writes the service results to the http response
type BookApiController struct {
	service      BookApiServicer
	errorHandler ErrorHandler
}

// BookApiOption for how the controller is set up.
type BookApiOption func(*BookApiController)

// WithBookApiErrorHandler inject ErrorHandler into controller
func WithBookApiErrorHandler(h ErrorHandler) BookApiOption {
	return func(c *BookApiController) {
		c.errorHandler = h
	}
}

// NewBookApiController creates a default api controller
func NewBookApiController(s BookApiServicer, opts ...BookApiOption) Router {
	controller := &BookApiController{
		service:      s,
		errorHandler: DefaultErrorHandler,
	}

	for _, opt := range opts {
		opt(controller)
	}

	return controller
}

// Routes returns all the api routes for the BookApiController
func (c *BookApiController) Routes() Routes {
	return Routes{
		{
			"BookGetBook",
			strings.ToUpper("Get"),
			"/book",
			c.BookGetBook,
		},
	}
}

// BookGetBook - Get book details
func (c *BookApiController) BookGetBook(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query()
	qParam := query.Get("q")
	filterParam := query.Get("filter")
	downloadParam := query.Get("download")
	startIndexParam, err := parseInt32Parameter(query.Get("startIndex"), false)
	if err != nil {
		c.errorHandler(w, r, &ParsingError{Err: err}, nil)
		return
	}
	maxResultsParam, err := parseInt32Parameter(query.Get("maxResults"), false)
	if err != nil {
		c.errorHandler(w, r, &ParsingError{Err: err}, nil)
		return
	}
	printTypeParam := query.Get("printType")
	orderByParam := query.Get("orderBy")
	result, err := c.service.BookGetBook(r.Context(), qParam, filterParam, downloadParam, startIndexParam, maxResultsParam, printTypeParam, orderByParam)
	// If an error occurred, encode the error with the status code
	if err != nil {
		c.errorHandler(w, r, err, &result)
		return
	}
	// If no error, encode the body and the result code
	EncodeJSONResponse(result.Body, &result.Code, w)

}
 

This is the implementation of a server-side API for the BookApi service. It is written in Go and uses the net/http package for handling HTTP requests and responses.

The code defines a BookApiController struct which is responsible for binding HTTP requests to the API service and writing the service results to the HTTP response. The controller takes an instance of the BookApiServicer interface as its service property, which defines the API actions that the controller will call when handling requests.

The Routes method of the BookApiController returns an array of route definitions, where each route maps an HTTP endpoint to a specific handler function in the controller. In this case, there is only one endpoint "/book" which is handled by the BookGetBook function.

The BookGetBook function extracts the query parameters from the URL of the incoming HTTP request, parses them and passes them as arguments to the BookGetBook method of the BookApiServicer interface. The results of the service call are written to the HTTP response using the EncodeJSONResponse function, or an error is returned in case of an error.

The NewBookApiController function creates a new instance of the BookApiController and sets it up with the given options. The WithBookApiErrorHandler option is used to inject an instance of an error handler into the controller. This error handler is used to handle any errors that occur during the handling of requests.

 /*
 * BookApi API (beta ver.)
 *
 *  # Authentication simple no api access, no access token, just a toy project
 *
 * API version: 1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package openapi

import (
	"context"
	"errors"
	"fmt"
	"io"

	// "errors"
	"encoding/json"
	"net/http"
)

// BookApiService is a service that implements the logic for the BookApiServicer
// This service should implement the business logic for every endpoint for the BookApi API.
// Include any external packages or services that will be required by this service.
type BookApiService struct {
}

// NewBookApiService creates a default api service
func NewBookApiService() BookApiServicer {
	return &BookApiService{}
}

// BookGetBook - Get book details
func (s *BookApiService) BookGetBook(ctx context.Context, q string, filter string, download string, startIndex int32, maxResults int32, printType string, orderBy string) (ImplResponse, error) {
	// TODO - update BookGetBook with the required logic for this service method.
	// Add api_book_service.go to the .openapi-generator-ignore to avoid overwriting this service implementation when updating open api generation.
	// GET API KEY FOR GOOGLE BOOKS API
	baseUrl := "https://www.googleapis.com/books/v1/volumes"
	req, err := http.NewRequest("GET", baseUrl, nil)
	if err != nil {
		return Response(http.StatusNotImplemented, nil), errors.New("Need to provide a valid URL")
	}

	// if you appending to existing query this works fine
	query := req.URL.Query()
	key := GetEnvVar("GOOGLE_BOOK_API_KEY", "")
	if key == "" {
		return Response(http.StatusNotImplemented, nil), errors.New("Need to provide a valid API key")
	}
	query.Add("key", key)
	if q != "" {
		query.Add("q", q)
	} else {
		return Response(http.StatusNotImplemented, nil), errors.New("Need to provide a valid query")
	}

	// or you can create new url.Values struct and encode that like so
	if filter != "" {
		// add filter validation here
		query.Add("filter", filter)
	}
	if download != "" {
		// add download validation here
		query.Add("download", download)
	}

	// http.get
	resp, err := http.Get(req.URL.String() + "?" + query.Encode())
	if err != nil {
		// handle error
	}
	fmt.Println(req.URL.String() + "?" + query.Encode())
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	var bookResponse GoogleBookResponse
	err = json.Unmarshal(body, &bookResponse)
	// destructure body into GoogleBookResponse

	//TODO: Uncomment the next line to return response Response(200, {}) or use other options such as http.Ok ...
	return Response(200, bookResponse), nil

	// return Response(http.StatusNotImplemented, nil), errors.New("BookGetBook method not implemented")
}
 

This is a Go implementation for a simple book API. The API implements the BookGetBook function that retrieves information about a book from Google Books API.

The implementation uses the Go standard library to make an HTTP GET request to the Google Books API, retrieves the response, and unmarshals it into a Go struct GoogleBookResponse. The API key for the Google Books API is obtained from an environment variable.

The API returns an instance of the ImplResponse struct with the status code set to 200 and the body set to the GoogleBookResponse struct on success. In case of errors, the API returns an instance of the ImplResponse struct with the appropriate status code and an error message.

 /*
 * BookApi API (beta ver.)
 *
 *  # Authentication simple no api access, no access token, just a toy project
 *
 * API version: 1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package openapi

import (
	"encoding/json"
	"errors"
	"github.com/gorilla/mux"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"strconv"
	"strings"
)

// A Route defines the parameters for an api endpoint
type Route struct {
	Name		string
	Method	  string
	Pattern	 string
	HandlerFunc http.HandlerFunc
}

// Routes are a collection of defined api endpoints
type Routes []Route

// Router defines the required methods for retrieving api routes
type Router interface {
	Routes() Routes
}

const errMsgRequiredMissing = "required parameter is missing"

// NewRouter creates a new router for any number of api routers
func NewRouter(routers ...Router) *mux.Router {
	router := mux.NewRouter().StrictSlash(true)
	for _, api := range routers {
		for _, route := range api.Routes() {
			var handler http.Handler
			handler = route.HandlerFunc
			handler = Logger(handler, route.Name)

			router.
				Methods(route.Method).
				Path(route.Pattern).
				Name(route.Name).
				Handler(handler)
		}
	}

	return router
}

// EncodeJSONResponse uses the json encoder to write an interface to the http response with an optional status code
func EncodeJSONResponse(i interface{}, status *int, w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	if status != nil {
		w.WriteHeader(*status)
	} else {
		w.WriteHeader(http.StatusOK)
	}

	return json.NewEncoder(w).Encode(i)
}

// ReadFormFileToTempFile reads file data from a request form and writes it to a temporary file
func ReadFormFileToTempFile(r *http.Request, key string) (*os.File, error) {
	_, fileHeader, err := r.FormFile(key)
	if err != nil {
		return nil, err
	}

	return readFileHeaderToTempFile(fileHeader)
}

// ReadFormFilesToTempFiles reads files array data from a request form and writes it to a temporary files
func ReadFormFilesToTempFiles(r *http.Request, key string) ([]*os.File, error) {
	if err := r.ParseMultipartForm(32 << 20); err != nil {
		return nil, err
	}

	files := make([]*os.File, 0, len(r.MultipartForm.File[key]))

	for _, fileHeader := range r.MultipartForm.File[key] {
		file, err := readFileHeaderToTempFile(fileHeader)
		if err != nil {
			return nil, err
		}

		files = append(files, file)
	}

	return files, nil
}

// readFileHeaderToTempFile reads multipart.FileHeader and writes it to a temporary file
func readFileHeaderToTempFile(fileHeader *multipart.FileHeader) (*os.File, error) {
	formFile, err := fileHeader.Open()
	if err != nil {
		return nil, err
	}

	defer formFile.Close()

	fileBytes, err := ioutil.ReadAll(formFile)
	if err != nil {
		return nil, err
	}

	file, err := ioutil.TempFile("", fileHeader.Filename)
	if err != nil {
		return nil, err
	}

	defer file.Close()

	file.Write(fileBytes)

	return file, nil
}

// parseInt64Parameter parses a string parameter to an int64.
func parseInt64Parameter(param string, required bool) (int64, error) {
	if param == "" {
		if required {
			return 0, errors.New(errMsgRequiredMissing)
		}

		return 0, nil
	}

	return strconv.ParseInt(param, 10, 64)
}

// parseInt32Parameter parses a string parameter to an int32.
func parseInt32Parameter(param string, required bool) (int32, error) {
	if param == "" {
		if required {
			return 0, errors.New(errMsgRequiredMissing)
		}

		return 0, nil
	}

	val, err := strconv.ParseInt(param, 10, 32)
	if err != nil {
		return -1, err
	}

	return int32(val), nil
}

// parseBoolParameter parses a string parameter to a bool
func parseBoolParameter(param string) (bool, error) {
	val, err := strconv.ParseBool(param)
	if err != nil {
		return false, err
	}

	return bool(val), nil
}

// parseInt64ArrayParameter parses a string parameter containing array of values to []int64.
func parseInt64ArrayParameter(param, delim string, required bool) ([]int64, error) {
	if param == "" {
		if required {
			return nil, errors.New(errMsgRequiredMissing)
		}

		return nil, nil
	}

	str := strings.Split(param, delim)
	ints := make([]int64, len(str))

	for i, s := range str {
		if v, err := strconv.ParseInt(s, 10, 64); err != nil {
			return nil, err
		} else {
			ints[i] = v
		}
	}

	return ints, nil
}

// parseInt32ArrayParameter parses a string parameter containing array of values to []int32.
func parseInt32ArrayParameter(param, delim string, required bool) ([]int32, error) {
	if param == "" {
		if required {
			return nil, errors.New(errMsgRequiredMissing)
		}

		return nil, nil
	}

	str := strings.Split(param, delim)
	ints := make([]int32, len(str))

	for i, s := range str {
		if v, err := strconv.ParseInt(s, 10, 32); err != nil {
			return nil, err
		} else {
			ints[i] = int32(v)
		}
	}

	return ints, nil
} 

This code is an implementation of a simple RESTful API in Go. The code uses the Gorilla Mux library for routing and handling HTTP requests. The Routes type defines a collection of endpoints for the API, and the Router interface defines the method for retrieving these routes. The NewRouter function creates a new router using the Gorilla Mux library and allows for any number of routers to be passed in. The EncodeJSONResponse function is used to write a response in JSON format to the HTTP response, and the ReadFormFileToTempFile and ReadFormFilesToTempFiles functions are used to read file data from an HTTP request form and write it to a temporary file. The parseInt64Parameter and parseInt32Parameter functions are used to parse string parameters to their respective integer types.

 /*
 * BookApi API (beta ver.)
 *
 *  # Authentication simple no api access, no access token, just a toy project
 *
 * API version: 1
 * Generated by: OpenAPI Generator (https://openapi-generator.tech)
 */

package main

import (
	"log"
	"net/http"

	openapi "github.com/FriendlyUser/bookapi/go"
)

func main() {
	log.Printf("Server started")

	BookApiService := openapi.NewBookApiService()
	BookApiController := openapi.NewBookApiController(BookApiService)

	router := openapi.NewRouter(BookApiController)
	// get PORT from PORT variable
	port := openapi.GetEnvVar("PORT", "8080")
	log.Fatal(http.ListenAndServe(":" + port, router))
}
 

This is a main function for a Go program that implements a simple API for a book collection. The API has no authentication and is just a toy project.

The program starts by logging a message that the server has started. It then creates instances of the BookApiService and BookApiController from the openapi package.

Next, the program creates an instance of the Router from the openapi package and sets it as the handler for HTTP requests. The port for the server to listen on is determined by either the PORT environment variable or a default value of “8080”.

Finally, the program starts an HTTP server and listens on the specified port. The ListenAndServe function will block until the server is shut down or an error occurs.

Using a cloud provider like AWS, Google Cloud, or Microsoft Azure to host your API can be helpful for a number of reasons:

  1. Scalability: Cloud providers have infrastructure that allows you to easily scale your API up or down depending on your usage. This can be particularly helpful for handling spikes in traffic.
  2. Reliability: Cloud providers have multiple data centers and use load balancing techniques to ensure that your API is highly available.
  3. Security: Cloud providers invest heavily in security, so you can benefit from the security measures they put in place, such as firewalls, encryption, and access controls.
  4. Cost: Hosting your API in the cloud can be more cost-effective than maintaining your own infrastructure, especially if you are just starting out or have a limited budget.
  5. Management: Cloud providers offer various management and monitoring tools that can help you manage your API more efficiently.

By hosting your API on a cloud platform, you can focus on building and improving your API while the cloud provider takes care of the infrastructure, security, and scaling.

The Spacefile in the root of your project is used to define the configuration for your project to deployment to deta space. My Spacefile is a YAML file that contains the following sections:

 v: 0
micros:
  - name: go-app
    src: .
    engine: custom
    commands:
      - go get
      - go build main.go
    run: ./main
    include:
      - main
    presets:
      env:
        - name: GOOGLE_BOOK_API_KEY
          description: Google Books API Key 

This is a definition for a microservice in the format of a Micros configuration file. This microservice, named “go-app,” will run a Go application. The source code for the application is in the current directory (.), and the build process involves executing the go get and go build main.go commands. The application is then executed with the ./main command. The environment variable “GOOGLE_BOOK_API_KEY” is also specified and its purpose is described as being the Google Books API Key.

References

© Copyright 2024 by FriendlyUsers Tech Blog. Built with ♥ by FriendlyUser. Last updated on 2024-04-18.