Project Overview

What is go-nvd?

go-nvd is an unofficial API wrapper for interacting with the nvd.nist.gov REST APIs. The main appeal of the project is the declarative programming style it enables using WithFunctions and an easy to configure client enables the developer to interact with the NVD without contributing to code complexity. This reduces noise and allows developers to focus entirely on how they analyse, modify or otherwise process the response data. As an Example, the code to request for 300 CVEs can be as simple and concise as:

package main

import (
	"codeberg.org/chillygopher/go-nvd"
	"codeberg.org/chillygopher/go-nvd/cve"
)

// main fetches a total of 300 CVEs using the resultsPerPage Parameter
func main() {
	client := cve.NewClient(gonvd.WithAPIKey("INSERT-API-KEY"))

	resp, err := client.FilteredFetch(cve.WithResultsPerPage(300))
	if err != nil {
	 // handle error
	}
	
	// use resp as required
}

API Coverage

go-nvd fully supports interacting with both the CVE and CVE History API (and also the Feeds API, but the support for it is rather experimental), with wrappers for the other APIs on their way as the library matures and grows.

The Design Philosophy - Levering Functional Options

Overview

The design philosophy for the go-nvd API revolves heavily around the Client which is used to interact with their respective NVD REST API endpoint, Each Client exposes a handful of helper methods for that specific API Endpoint and more importantly the FilteredFetch Method, Below is the FilteredFetch Method implemented by the cveClient;

func (c *cveClient) FilteredFetch(opts ...FilterOpts) (ResponseType, error)

Fetching the Information

To fetch the information, the User should primarily use the FilteredFetch method. The method expects the user to enter a variable number of types that satisfy the FilterOpts Interface (An Interface can simply be considered as a contract for types, If the types implement the methods for that interface than the type is said to satisfy that Interface).

This method follows the Functional Options design pattern as defined by the Style guide developed by Uber (Find the Entire Guide Here).

Functional Options as defined by the Style guide;

Functional options is a pattern in which you declare an opaque Option type that records information in some internal struct. You accept a variadic number of these options and act upon the full information recorded by the options on the internal struct.

Use this pattern for optional arguments in constructors and other public APIs that you foresee needing to expand, especially if you already have three or more arguments on those functions.

The FilterOpts Interface is;

type FilterOpts interface {
	apply(vals *url.Values) error
}

This enables users to add a variadic number of parameters to their request, which is then applied to the query URL, enabling users to form URL-encoded queries such as; https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=300 using the function call;

resp, err := client.FilteredFetch(cve.WithResultsPerPage(300))

All the parameters for the CVE and CVE History REST APIs are supported, read either the package specification on https://pkg.go.dev/codeberg.org/chillygopher/go-nvd or read the API references on the NVD website https://nvd.nist.gov/developers.

Configuring the Client

A Client can also be configured using a similar approach, A constructor function for a CVE Client is shown below;

func NewClient(opts ...gonvd.ClientOption) *cveClient

Possible Configurations allow the client to;

  • Retry requests when rate limited
  • Apply an API key
  • Apply a Timeout

Challenges

Similarly to most projects, go-nvd has had its fair share of challenges and obstacles, I will briefly mention some of the more prominent challenges I encountered and the solutions I employed.

The Retry Riddle

Originally, While Implementing the Retry functionality to retry after being rate limited, I initially intended to use the Retry-After HTTP Header to determine the amount of time the library waited before retrying, However, after testing with curl, I discovered the NVD doesn’t even return a Retry-After Header when the requests are rate limited! This was problematic because this required me to guess how long the client must wait before retrying the request, To address this, the client has been implemented to employ a two-second back off to circumvent users from accidentally spamming the NVD in an intolerable manner.

The NewValue and OldValue Oddities

For the CVE History API, The NVD returns a Changes object with both NewValue and OldValue fields, Both of these values can be either; a string, an array of strings or a JSON Object, This while being practical for the NVD makes it a nightmare to handle the return type, One Approach may have been to check for all the different changes the NVD could have made and have a go type correspond to the returned type.

However, this makes the code complex and therefore twice as hard to maintain and also makes the go-nvd interface seem complex and very challenging for a User using the library.

So, to circumvent both these issues, I declare type valueHistory []string and implement an UnmarshalJSON method (Which is automatically called by the json Unmarshal Functions and Decoding Methods), allowing me to store a string, a slice (a slice is a dynamically allocated array) of strings or an object converted to a string, while allowing the user to handle the valueHistory type like a normal string slice.

The Constant DRY Duel

DRY (Don’t Repeat Yourself) is a programming principle, which emphasises that a programmer should try to minimise the amount of code repeated in their library or program. While this is a great concept, It is one that requires one to really put on their engineering cap on, For Example, In the earlier versions of go-nvd (pre-v0.4.0), I repeated a large amount of code for configuring the client and other internal methods which could have easily been shared among the cve and the cvehistory sub-packages, So, In v0.4.0, I refactored this entirely and levereged an embedded Client which held the common configuration for the package, this required me to learn concepts like struct Embedding, Composition and Type scopes in even more detail than I had ever before.

While the code still is not perfect, nor completly DRY compliant, it is progressively evolving in a better state as the DRY compliance struggle is a constant one.

My Gains from go-nvd

Like all projects, this project has taught me and motivated me to learn a lot more, when I began with this project in June of 2026 I did not have a great grasp on Go code compared to what I know now, and I look forward to learn more while I continue to work on this project while starting others. I have learned a large deal about Go design & development, REST API Clients, HTTP, JSON handling, Programming Workflows, Open Source Development and Countless other concepts that I will carry on to other projects.

While the project has room for improvement, it is significantly better compared to when I had initially started developing and I am pleased with the progress I have made.

Thank you for reading this short project breakdown and review, Hope you have a great day ahead!