Golang. CRUD in REST API in a generic way.

Yuri Fenyuk
7 min readApr 17, 2023

--

Building REST API and implementing CRUD operations are among the most common tasks for Backend developers. Golang started to support generics not so long ago (with version 1.18) which gave an idea to use this language feature to stop doing repetitive tasks for numerous simple Entities that frequently need to be exposed to Frontend as REST API endpoints. In the beginning, I will go the traditional way with one Entity to see what it takes to get the job done. After it, the approach will be generalized to have reusable code and another Entity will be coded.

In the new folder, run following:

#2: use Gorilla’s mux request router.

#3: viper package is used to keep application parameters in configuration file.

Open project folder in VSCode and create config.json in the root:

Below go-file is needed to programmatically read the configuration (despite the fact it only has the port number at the moment, the set will grow with time):

#15..28: function needs to be called to read configuration from the file into the program.

#13: AppConfig variable is needed to be used to access values from JSON file (for ex., AppConfig.Port has “6000” port value)

Let’s code CRUD for an imaginary Product Entity without any cheating etc. This entity looks like following (put code into new entities/product.go file):

Golang slice will be used as a Repository for Product. This allows do not spend time on building a proper persistence layer as this is less important for the current experiment. Still, better to extract Repository into separate module and wrap access to storage slice with traditional CRUD methods (put code into new repos/product-repo.go file):

The file is quite long but the CRUD implementation is absolutely usual.

#5: Product Entity will be extensively used through the file, so need to reference it.

#8..#10: Define struct ProductRepo holding slice of Product as a surrogate of storage.

#12..#14: Typical for Golang function for proper new instance creation.

#16..#56: Product CRUD operations.

Controllers play a central role in current backend architecture since they have code to handle HTTP requests by interacting with abstracted third-party data providers. In this case, there is only ProductRepo repository.

Product controller might look like following (put code into new controllers/product-controller.go file):

Functions name are self-explanatory and support new Product creation (#15), getting all Products (#25), getting only one Product (#31), updating Product (#49), deleting Product (#67).

All preparations are done and the final step is to create HTTP server with help of Gorilla’s mux. For simplicity, the rest of the code will be directly in main.go:

#13: Load configuration from JSON file.

#16: Create an instance of Gorilla/mux router which is a bridge between HTTP endpoint URLs and Controller methods.

#18, #25..#32: Filling router with mapping.

#22: Start REST API server on IP-port specified in config file.

Once more, there are 4 parts that build up REST API server:

  • Product model (entities/product.go) which describes parameters of the entity.
  • Repository for Product (repos/product-repo.go) maintaining Product set via obvious access functions.
  • Controller for Product (controllers/product-controller.go) which incorporates Repository and implements a data flow between REST API calls and the Repository.
  • REST API server (main.go) which creates the router with links to the corresponding Controller’s method and runs this server.

The project structure in VS Code should look similar to the following:

Current project structure

I use VS Code extension named REST Client to quickly test endpoints (sure Go program also needs to be run) and this simple file, which, after it is opened in VS Code, has tiny Send request links before each section (the result of Create Product, followed by Get All Products is presented on screenshot):

Testing Product endpoints

It is important to understand that the addition of every new similar to Product entity (Order, Client, whatever) results in 3 new go-files which are a bunch of very similar but incompatible code.

This observation is a good driver to come up with reusable code which will work for different entities without modification. And Controller here is the first candidate for optimization. Luckily, Golang generics is the language feature that might come in handy.

After a longer look into Gorilla mux router entries:

… the one can notice that only two combinations of mux URL (/api/products and /api/products/{id}) are possible for all five standard manipulation operations and these operations (except two) have different request verbs (GET, POST, PUSH, DELETE). Those two entries which both have GET are differed by {id} suffix in mux URL. These observations should be sufficient to squeeze five different methods of Product Controller into one longer function.

The complicating factor is the usage of Repository methods all over the Controller. Luckily, it can be abstracted away with the help of Golang Interface (for simplicity I assume that all entities will have the need to support full CRUD operation and the only identity has unit type). Future Repository interface should “work” for different entities (Product, Order, Client, etc.), so it must be parametrized with a generic parameter ( which in most languages looks like [T]).

Having the thoughts from the two previous paragraphs in mind, the plan is to create generic code (by borrowing pieces from Product entity above and making it reusable) and implement REST API for Brand entity.

The new Brand entity is as simple as following(put it into new file entities/brand.go):

Repository generic interface is following (put it into new file repos/generic-repo.go):

where parameter T can be anything (any), including Brand struct described above. CRUD functions are easy to understand and interfaces enforces input/output parameters to have correct types.

Let’s implement Brand concrete Repository. The interface implementation in Golang is implicit, so needs to look in the generic interface definition, use Brand struct everywhere where T is used in interface and reuse the existing Product Repository code (keep all entity-set in slice and manipulate with its content inside CRUD operations). Put the following code into new repos/brand-repo.go:

And finally, the generic Router might be defined like the following:

#1: defining generic struct with parameter TT which can be anything (thinking about it as Brand entity defined above) and parameter T which constraint to implement GenericRepo interface which, in turns, parametrized with first parameter TT (thinking about it as BrandRepo repository defined above and using Brand entity in every place where GenericRepo interface use parameter).

#2: struct has muxBase string which uniquely identifies REST API resource on server. Something like api/products, api/brands, etc.

#3: need to keep the pointer to Repository as this router directly manipulates with data storage.

Next method plays the central role in generic Router:

#2: Method’s signature is nothing unexpected at all, http.ResponseWriter and http.Request.

#3: Try to extract {id} parameter as unit value. (remember analysis I made before generalization code section: for 2 out of 5 CRUD operations we must have {id}).
#4..#7: If there is something in URL but not {id} — stop processing.

#10..#23: If HTTP verb is GET, need to process get single Entity or get all Entities by querying corresponding concrete Repository methods.

#24..#28: handling create new Entity.

#29..#38: handling update Entity.

#39..#48: handling delete Entity.

Next step is to have the method to register handler function to allowed router endpoints:

And the last step is to have a proper new instance of struct creator:

#4..#5: initialize Router with parameters passed from outside.

#7: do not forget to call routeRegister(…) otherwise handle(…) will not be called at all.

All the above four parts of GenericRouter struct should append one to other and stored in new file generic-router.go.

GenericRouter is completed and can be used to implement REST API for Brands. main.go will have a new function (which needs to be called inside main(…) ):

#3: New instance of BrandRepo implements implicitly GenericRepo interface for Brand entity since all CRUD operations receive Brand entities where interface requires T param and returns either T or slice of T. Thus assignment to variable of concrete repos.GenericRepo[entities.Brand] works.

#4: New instance of GenericRouter with concrete parameters Brand entity (instead TT in GenericRouter) and BrandRepo (instead of T in GenericRouter which is GenericRepo[Brand] at the same time) is created. As parameters it receives muxBase equals to ‘/api/brands’ and initialized BrandRepo (this 3rd party dependency should be implemented in a serious way storing data in DB, etc.).

If the will be a need to support one more entity, let’s say Employee following needs to be done:

  • describe Employee entity as Golang struct.
  • implement EmployeeRepo which fulfills GenericRepo interface.
  • create GenericRouter specifying Employee and EmployeeRepo instead of generic parameters and set muxBase equals to ‘/api/employees’.

Thus, as a minimum, developers can reuse GenericRouter already. Repositories can also be refactored to extract generic Repository where Entity is parameter T and this brings more efficiency in coding.

Structure after adding Brands REST API

The potential next step might be to migrate Product REST API implementation from the traditional way (first half of the article) to generic (second half of article) which allows to drop couple files and reduce codebase.

The final version of the project is in here.

Golang generics is the long-awaited feature of the language. It is not so difficult to grasp and start using as most of ‘older brothers’ have had it for quite some time. The generics can be a push to refactor some existing modules to increase the level of code reuse. More important, generics allow new code creation with generic parameters from the very beginning. The balance of code simplicity and code compact size is rather critical to keep.

--

--