Blog Cover

Create a fully functional API server in Go using only the standard library in under 5 minutes

Author profile image
Aitor Alonso

Oct 28, 2023

Updated Nov 26, 2023

4 min read

If this is not the first article you read about Golang (Go), no matter the author, you probably already know that Go is a programming language that was created to be simple, fast, and efficient. And it was a language born for the web, so it has a very powerful standard library for web development.

In Go you can, only using the standard libs, to create a web server, a web client, and even a proxy server. In other languages, such as Node.js (where part of my background is), you need third party libs such as express to create a web server, and axios to create a resilient web client. However, you don't need any third party libs to create a web server or a web client in Go.

Let's see some snippets that could help you to start prototyping your next API.

Handlers and Middlewares

Handlers are just functions that receive a http.ResponseWriter and a *http.Request as parameters. The http.ResponseWriter is used to write the response to the client, and the *http.Request is used to read the request from the client. We'll see more about these two types later.

// Standard HTTP handler function signature.
func myHandler(w http.ResponseWriter, r *http.Request) {
	// Your code here

And what about middlewares? Middlewares are just functions that receive a http.Handler as parameter and return a http.Handler. To continue the request execution, you need to call the ServeHTTP method of the http.Handler that you received as parameter.

// Standard middleware function signature.
func myMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Your middleware code here
		next.ServeHTTP(w, r) // Continue the request execution

http.Request and http.ResponseWriter

Now, let's do a real quick overview of the http.Request and http.ResponseWriter types.

The http.Request type is a struct that contains all the information about the request that the client made to the server. It contains the request method, the request URL, the request headers, the request body, and much more.

ip := r.RemoteAddr // Get the IP address that sent the request.
method := r.Method // Get the request method.
path := r.URL.Path // Get the request URL path.

id := r.URL.Query().Get("id")    // Get the query string parameter "id".
accept := r.Header.Get("Accept") // Get the request "Accept" header.

body, err := ioutil.ReadAll(r.Body) // Read the request body into a []byte slice

And the http.ResponseWriter type is an interface that allows us to write the response to the client. It contains methods to set the response headers, to set the response status code, and to write the response body.

// Remember: All headers must be set before calling w.WriteHeader() or w.Write().
w.Header().Set("Location", "/article/1")

There are also some shortcuts to write error responses.

http.NotFound(w, r)
http.Error(w, "Bad Request", 400)
// Or the same using constants
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)

Some useful constants are:

http.MethodGet  // "GET"
http.MethodPost // "POST"

http.StatusMovedPermanently    // 301
http.StatusTemporaryRedirect   // 307
http.StatusBadRequest          // 400
http.StatusUnauthorized        // 401
http.StatusForbidden           // 403
http.StatusNotFound            // 404
http.StatusMethodNotAllowed    // 405
http.StatusInternalServerError // 500

Creating the server

Okay, now we know how to create handlers and middlewares, and how to read requests and write responses. Let's see how to create a server.

First, we need to define a http.ServeMux to register our handlers. A http.ServeMux is a struct that acts as the router of our server.

mux := http.NewServeMux()
mux.HandleFunc("/foo", fooHandler)  // Matches the path "/foo" only.
mux.HandleFunc("/foo/", barHandler) // Matches all paths that begin with "/foo/".

// Register a static file server for all URL paths that begin "/static/".
fs := http.FileServer(http.Dir("./path/to/static/dir"))
mux.Handle("/static/", http.StripPrefix("/static", fs))

Methods are checked inside the handlers. For example, imagine that the fooHandler should only handle GET requests. You can check the request method inside the handler and return an error if the method is not the expected.

func fooHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)

	// Your code here

And finally, we need to create a http.Server and call the ListenAndServe method to start the server.

srv := &http.Server{
	Addr: ":4080", // localhost:4080
	Handler: mux,
	ErrorLog: log.New(...),
	IdleTimeout: time.Minute,
	ReadTimeout: 5 * time.Second,
	WriteTimeout: 10 * time.Second,

err := srv.ListenAndServe()

Adding context to requests

Context is a fundamental concept in Go. It allows us to pass data between functions and goroutines. And it's very useful to pass data between middlewares and handlers. You can add context to requests too using the http.Request.WithContext method.

type contextKey string
var contextKeySite = contextKey("site")

// Add a value to request context. Note: This creates a new copy of the request.
ctx = context.WithValue(r.Context(), contextKeySite, "")
r = r.WithContext(ctx)

// Retrieve the value from request context.
site, ok := r.Context().Value(contextKeySite).(string)

Wrapping up

And that's it. As you saw, the net/http package has everything you need to start prototyping your next API. You can create a server, add handlers and middlewares, read requests, write responses, and add context to requests just using the standard library.

Now you are ready to start prototyping your next API in Go. Do you want more? I wrote a similar article, with all you need to know to integrate a SQL database in golang. Check it out!

I hope my article has helped you, or at least, that you have enjoyed reading it. I do this for fun and I don't need money to keep the blog running. However, if you'd like to show your gratitude, you can pay for my next coffee(s) with a one-time donation of just $1.00. Thank you!