Build a Bookmark API in Go
Build a REST API in Go using only the standard library.
Introduction
30 Second Summary
Everyone saves links they want to read later. What if you could build the backend that powers a bookmark manager yourself?
In this project, you will build a REST API in Go that stores, retrieves, and creates web bookmarks. You'll use only Go's standard library with zero external dependencies.
What You'll Build
A bookmark manager API with three HTTP endpoints for listing, retrieving, and creating bookmarks, all powered by Go's standard library.
By the end of this project, you'll have:
- A working Go module with a compiled binary you can run anywhere on your machine.
- An HTTP server with pattern-based routing and JSON request/response handling.
- Hands-on experience with Go's core patterns: structs, slices, error handling, and HTTP handlers.
- Secret Mission: Add a DELETE endpoint and search-by-tag filtering to your API.
Are there any prerequisites?
None. This guide includes full setup instructions for Go and your code editor. You don't need any prior Go experience.
Before We Start
Before We Start...
Install Go and Set Up Your Editor
Before you can build anything in Go, you need the Go toolchain installed on your machine and a code editor to write your code in.
In this step, you will install Go 1.26, verify it is working, and set up Cursor with a project folder ready to go.
In this step, get ready to:
- Install Go on your machine.
- Verify Go is ready in your terminal.
- Create and open your project folder in Cursor.
Download and install Go
Go is the programming language you will use to build your API. Let's check whether it is already installed on your machine.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check if Go is already installed by running this command:
go version
Look at the output and pick the tab that matches what you see.
✔️ I see go1.26
You are all set. Your output looks something like go version go1.26.x followed by your operating system and architecture. Move on to the next substep.
ⓧ I see an older version
You have Go installed, but you need version 1.26 for this project. The new installer will replace your older version automatically.
macOS
- Open https://go.dev/dl/ in your browser.
- Download the .pkg installer for your Mac. Choose Apple macOS (ARM64) if you have an Apple Silicon Mac (M1/M2/M3/M4), or Apple macOS (x86-64) if you have an Intel Mac.
- Double-click the downloaded .pkg file and follow the on-screen prompts to complete the installation.
Not sure which Mac you have?
Click the Apple menu in the top-left corner of your screen, then click About This Mac. If you see "Apple M1" or similar under Chip, choose ARM64. If you see "Intel", choose x86-64.
Windows
- Open https://go.dev/dl/ in your browser.
- Download the .msi installer under Microsoft Windows.
- Run the downloaded .msi file and follow the installer prompts. Accept all defaults.
- Close your terminal and open a new one so the updated PATH takes effect.
- Verify the upgrade by running:
go version
You should now see go version go1.26.x in the output.
ⓧ Command not found
Go is not installed yet. Let's fix that.
macOS
- Open https://go.dev/dl/ in your browser.
- Download the .pkg installer for your Mac. Choose Apple macOS (ARM64) if you have an Apple Silicon Mac (M1/M2/M3/M4), or Apple macOS (x86-64) if you have an Intel Mac.
- Double-click the downloaded .pkg file and follow the on-screen prompts to complete the installation.
Not sure which Mac you have?
Click the Apple menu in the top-left corner of your screen, then click About This Mac. If you see "Apple M1" or similar under Chip, choose ARM64. If you see "Intel", choose x86-64.
Windows
- Open https://go.dev/dl/ in your browser.
- Download the .msi installer under Microsoft Windows.
- Run the downloaded .msi file and follow the installer prompts. Accept all defaults.
- Close your terminal and open a new one so the PATH is picked up.
- Verify the installation by running:
go version
You should see go version go1.26.x followed by your operating system and architecture.
Still seeing command not found after installing?
Make sure you opened a brand new terminal window after installation. Existing terminals do not pick up PATH changes automatically.
On macOS, the .pkg installer adds Go to /usr/local/go/bin and updates your PATH. If it still does not work, try running export PATH=$PATH:/usr/local/go/bin in your terminal, then retry go version.
Help me troubleshoot my Go PATH
Open your editor and create the project folder
You need a code editor to write your Go code. This project uses Cursor, but VS Code works too since they share the same foundation.
- If you do not have Cursor or VS Code installed, download Cursor from https://cursor.com/download.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor and press Enter to open it.
Now create your project folder. Switch back to the terminal you used earlier.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir bookmarks-api
cd bookmarks-api
- Confirm the folder exists by running:
ls ~/Desktop
You should see bookmarks-api in the output.
Now open this folder in Cursor.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the bookmarks-api folder, then click Open.
What should I see?
Cursor opens with the bookmarks-api folder name visible in the Explorer sidebar on the left. The folder is empty for now.
Don't see an Open Folder option?
If you are using a newer version of Cursor and see an Agent view instead of a traditional editor layout, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Open Folder, and select the command from the dropdown.
Help me open my project folder in Cursor
Your environment is ready. Next up, you will initialize a Go module and write your first lines of Go code.
Initialize Your Go Module
Your tools are in place. Now it's time to set up the foundation that every Go project needs before you can write any code.
Every Go project starts with a module. A module tells Go the name of your project and which version of Go you're using. Think of it like a project registration form that Go reads before doing anything else.
In this step, get ready to:
- Initialize a Go module in your project folder.
- Create a main.go file with a basic program.
- Run your program to confirm everything works.
Open the integrated terminal and initialize your module
Go needs a go.mod file to know your project's name and track any dependencies you add later. You create this file by running a single command.
- In your editor, open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Initialize your Go module by running this command:
go mod init bookmarks-api
What does this command do?
go mod init bookmarks-api creates a go.mod file in your current folder. This file declares your project's module name (bookmarks-api) and the Go version you're using.
Since this project uses only Go's standard library, you won't see any dependency entries in go.mod. That's intentional. Go's standard library is powerful enough to build a full API server without any external packages.
- Confirm the file was created by running:
ls
You should see go.mod listed in the output.
Don't see go.mod?
Make sure your terminal is inside the bookmarks-api folder. If you see a different directory in your terminal prompt, navigate to the correct folder with cd ~/Desktop/bookmarks-api and try the command again.
If you see "command not found", Go may not be installed correctly. Go back to Step 1 and verify your installation.
help me fix my go mod init command
Create your first Go file
With the module ready, you need an entry point for your program. In Go, every executable starts in a file that declares package main and contains a main function.
- Create a new file called main.go in your bookmarks-api folder.
- Add the following code to main.go:
package main
import "fmt"
func main() {
// Print a startup message to confirm the program runs
fmt.Println("Bookmarks API starting...")
}
What does this code do?
- package main tells Go this file is the entry point of an executable program.
- import "fmt" pulls in Go's formatting package, which includes functions for printing text.
- func main() is the function Go calls automatically when you run your program. Every Go executable must have exactly one.
- fmt.Println("Bookmarks API starting...") prints a message to your terminal to confirm the program ran successfully.
- Save main.go.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's what main.go should look like so far:
package main
import "fmt"
func main() {
fmt.Println("Bookmarks API starting...")
}
Run your program
Time to see your code in action. Go can compile and run your file in a single command.
- Run your program by entering this command in the terminal:
go run main.go
You should see Bookmarks API starting... printed in your terminal.
What does go run do?
go run main.go compiles your Go source code into a temporary binary and immediately executes it. It's a shortcut for development. You don't need to manually compile every time you want to test a change.
Seeing an error instead?
If you see cannot find package, make sure your file starts with package main (not package bookmarks or anything else).
If you see a syntax error, check that all brackets and quotes are properly closed. Go is strict about formatting.
help me debug my Go program
Your module is set up and your first Go program runs. Next, you'll define the data structure that represents a bookmark in your API.
Define Your Data Model
Your API needs a way to represent what a bookmark looks like. In Go, you define the shape of your data using structs.
In this step, you'll create a Bookmark struct and set up an in-memory data store pre-loaded with sample bookmarks for testing.
In this step, get ready to:
- Define a Bookmark struct with JSON tags.
- Create package-level variables for in-memory storage.
- Seed your data store with example bookmarks for testing.
Define the Bookmark struct
A struct in Go groups related fields together under one type. Each bookmark has an ID, a title, a URL, and a list of tags.
- In main.go, add the following struct definition between the import statement and the func main() function:
type Bookmark struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Tags []string `json:"tags"`
}
What does this code do?
The Bookmark struct defines the shape of every bookmark your API handles. Each field has a name, a type, and a struct tag.
The backtick annotations like json:"id" are struct tags. They tell Go's JSON encoder to use lowercase names (id, title, url, tags) when converting your struct to JSON for API responses.
- Save main.go.
- Run go run main.go in your terminal. You should still see Bookmarks API starting... printed with no errors.
Seeing a syntax error?
- Make sure the struct is placed outside of func main(). Structs are defined at the package level, not inside functions.
- Check that each struct tag uses backticks (`), not regular quotes.
- Verify each field has both a type and a tag on the same line.
Why won't my Bookmark struct compile?
Create your in-memory data store
Your API needs somewhere to keep bookmarks while the server is running. Go lets you declare package-level variables that live for the entire program's lifetime.
You'll create a slice to hold bookmarks, a counter for generating unique IDs, and a mutex for safe concurrent access.
- In main.go, find this import statement at the top of the file:
import "fmt"
- Replace it with this grouped import that adds the sync package:
import (
"fmt"
"sync"
)
- Now add the following variable declarations between your Bookmark struct and the func main() function:
var (
bookmarks = []Bookmark{
{ID: "1", Title: "Go Documentation", URL: "https://go.dev/doc", Tags: []string{"go", "docs"}},
{ID: "2", Title: "Go Blog", URL: "https://go.dev/blog", Tags: []string{"go", "blog"}},
}
nextID = 3
mu sync.Mutex
)
What does this code do?
- bookmarks is a slice of Bookmark structs. Think of it as a flexible list that can grow as users add bookmarks. It starts pre-loaded with two example entries so you can test right away.
- nextID is a counter set to 3 (since the seed data uses IDs 1 and 2). Each new bookmark gets the next number.
- mu is a sync.Mutex that protects the shared data from race conditions if multiple requests arrive at the same time.
- Save main.go.
- Run go run main.go in your terminal. You should still see Bookmarks API starting... with no errors. This confirms your struct, imports, and variables all compile correctly.
Getting an import error?
- If you see imported and not used: "sync", make sure your var block includes mu sync.Mutex. Go requires every imported package to be used.
- If you see a type error on bookmarks, make sure the Bookmark struct definition appears above the var block. Go reads top to bottom.
- Check that your grouped import uses parentheses, not curly braces.
Why is my Go import or variable block failing to compile?
✔️ Awesome, I've got everything!
Great. Double check you've saved main.go and confirmed it compiles with no errors.
ⓧ I'd like to double check the full code
Here's what your complete main.go should look like at this point:
package main
import (
"fmt"
"sync"
)
type Bookmark struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Tags []string `json:"tags"`
}
var (
bookmarks = []Bookmark{
{ID: "1", Title: "Go Documentation", URL: "https://go.dev/doc", Tags: []string{"go", "docs"}},
{ID: "2", Title: "Go Blog", URL: "https://go.dev/blog", Tags: []string{"go", "blog"}},
}
nextID = 3
mu sync.Mutex
)
func main() {
fmt.Println("Bookmarks API starting...")
}
Your data model is in place. Next up, you'll build the HTTP handlers that let users interact with these bookmarks through API endpoints.
Build the List and Get Endpoints
You have a data model and seed data ready. Now it is time to make that data accessible over HTTP.
In this step, you will write two handler functions that respond to incoming requests with JSON data. Then you will wire them into Go's built-in router and start a live server you can hit with curl.
In this step, get ready to:
- Create a handler that returns all bookmarks as JSON.
- Create a handler that returns a single bookmark by ID.
- Register both handlers on a router and start your HTTP server.
Create the list bookmarks handler
An HTTP handler in Go is a function that receives a request and writes back a response. Your first handler will return every bookmark in the slice as a JSON array.
- In main.go, find your import block at the top of the file:
import (
"fmt"
"sync"
)
- Replace the entire import block with this updated version that adds encoding/json and net/http:
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
- Now add the handleListBookmarks function below your main function by pasting this code at the bottom of the file:
func handleListBookmarks(w http.ResponseWriter, r *http.Request) {
// Lock the mutex so no other request can modify bookmarks mid-read
mu.Lock()
defer mu.Unlock()
// Tell the client we're sending JSON
w.Header().Set("Content-Type", "application/json")
// Encode the entire bookmarks slice as a JSON array
json.NewEncoder(w).Encode(bookmarks)
}
What does this code do?
mu.Lock() and defer mu.Unlock() protect the shared bookmarks slice from concurrent access. This is a standard Go pattern for safe concurrency.
json.NewEncoder(w).Encode(bookmarks) converts the entire slice into JSON and writes it directly to the HTTP response. No intermediate string needed.
- Save main.go.
- You will not see a visible change yet. The next substep adds the second handler, and then you will start the server to test both together.
Seeing red squiggles in your editor?
If your editor shows errors on the json or http references, double-check that your import block includes "encoding/json" and "net/http" exactly as shown above.
help me fix import errors in my Go file
Create the get bookmark handler
Your second handler returns a single bookmark by its ID. It uses Go's pattern-based routing to extract the ID directly from the URL path.
- Add the handleGetBookmark function below handleListBookmarks by pasting this code at the bottom of main.go:
func handleGetBookmark(w http.ResponseWriter, r *http.Request) {
// Lock the mutex for safe concurrent access
mu.Lock()
defer mu.Unlock()
// Extract the "id" segment from the URL path
id := r.PathValue("id")
// Search through bookmarks for a matching ID
for _, b := range bookmarks {
if b.ID == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(b)
return
}
}
// No match found - return a 404 error
http.Error(w, "bookmark not found", http.StatusNotFound)
}
What does this code do?
r.PathValue("id") pulls the value from the {id} segment in the URL pattern. If you request /bookmarks/2, it returns "2".
The for loop scans the slice until it finds a match. If it does, it encodes that single bookmark and returns immediately. If the loop finishes without a match, http.Error sends a 404 response.
- Save main.go.
- You will not see a change yet. The next substep wires these handlers into a running server.
Getting a 'declared and not used' error?
If you run go run main.go right now, Go may complain about unused variables. That is expected. The handler functions reference json and http through their parameter types, so the imports are fine. Any other error means a typo in the function signature.
help me fix a declared-and-not-used error in Go
Register handlers and start the server
With both handlers written, you need a router to connect URL patterns to functions and a server to listen for incoming requests.
- In main.go, find the import block at the top of the file and add "log" between "fmt" and "net/http":
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
)
- Now find your main function:
func main() {
fmt.Println("Bookmarks API starting...")
}
- Replace the entire main function with this updated version:
func main() {
// Create a new router
mux := http.NewServeMux()
// Register handler functions with URL patterns
mux.HandleFunc("GET /bookmarks", handleListBookmarks)
mux.HandleFunc("GET /bookmarks/{id}", handleGetBookmark)
fmt.Println("Bookmarks API server starting on http://localhost:8080")
// Start the server - log.Fatal will print any error and exit
log.Fatal(http.ListenAndServe(":8080", mux))
}
What does this code do?
http.NewServeMux() creates a router that maps URL patterns to handler functions. The pattern "GET /bookmarks" matches only GET requests to that exact path.
"GET /bookmarks/{id}" uses a wildcard segment. Go captures whatever value appears in that position and makes it available via r.PathValue("id").
http.ListenAndServe(":8080", mux) starts an HTTP server on port 8080. It blocks forever (until you stop it), so log.Fatal wraps it to print any startup error.
- Save main.go.
- Start the server by running this command in your terminal:
go run main.go
You should see Bookmarks API server starting on http://localhost:8080 printed in your terminal. The cursor stays on a blank line because the server is running and waiting for requests.
- Open a second terminal tab or window. Test the list endpoint by running:
curl http://localhost:8080/bookmarks
You should see a JSON array with your two seed bookmarks (Go Documentation and Go Blog).
- Test the get endpoint by running:
curl http://localhost:8080/bookmarks/1
You should see a single JSON object for the Go Documentation bookmark.
- Test a bookmark that does not exist by running:
curl http://localhost:8080/bookmarks/99
You should see bookmark not found as the response. This confirms your 404 handling works.
Not getting a response from curl?
Make sure your server is still running in the first terminal. Look for the Bookmarks API server starting on http://localhost:8080 message. If the terminal shows an error or returned to a prompt, the server crashed.
Check that you are running curl in a separate terminal from the one running the server. The server blocks its terminal while it is running.
help me debug why curl cannot connect to my Go server on port 8080
✔️ Awesome, I've got everything!
Great. Double check you have saved main.go and keep your server running for the next step.
ⓧ I'd like to double check the full code
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
)
type Bookmark struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Tags []string `json:"tags"`
}
var (
bookmarks = []Bookmark{
{ID: "1", Title: "Go Documentation", URL: "https://go.dev/doc", Tags: []string{"go", "docs"}},
{ID: "2", Title: "Go Blog", URL: "https://go.dev/blog", Tags: []string{"go", "blog"}},
}
nextID = 3
mu sync.Mutex
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /bookmarks", handleListBookmarks)
mux.HandleFunc("GET /bookmarks/{id}", handleGetBookmark)
fmt.Println("Bookmarks API server starting on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleListBookmarks(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bookmarks)
}
func handleGetBookmark(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
id := r.PathValue("id")
for _, b := range bookmarks {
if b.ID == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(b)
return
}
}
http.Error(w, "bookmark not found", http.StatusNotFound)
}
Your API is live and responding to requests. Next up, you will add the ability to create new bookmarks by handling POST requests with JSON bodies.
Build the Create Endpoint
Your API can list and retrieve bookmarks. Now you need a way to add new ones.
In this step, you'll build a POST endpoint that accepts JSON from the request body, validates the input, and stores a new bookmark in memory.
In this step, get ready to:
- Create a handler that decodes JSON request bodies into a Go struct.
- Add input validation that rejects incomplete bookmarks.
- Test the full create-and-list flow with curl.
Write the create bookmark handler
When a client sends a POST request, the server needs to read the JSON body, check that the required fields are present, assign an ID, and store the bookmark.
- In main.go, find your import block at the top of the file:
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
)
- Add "strconv" between "net/http" and "sync", so your import block looks like this:
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
What is strconv?
The strconv package converts between strings and other types. You'll use strconv.Itoa to convert your integer ID counter into a string for the bookmark's ID field.
- Now add the handleCreateBookmark function at the bottom of main.go, after the handleGetBookmark function:
func handleCreateBookmark(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
var b Bookmark
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if b.Title == "" || b.URL == "" {
http.Error(w, "title and url are required", http.StatusBadRequest)
return
}
b.ID = strconv.Itoa(nextID)
nextID++
bookmarks = append(bookmarks, b)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(b)
}
What does this code do?
This handler does four things in sequence:
- json.NewDecoder(r.Body).Decode(&b) reads the raw JSON from the request body and fills a Bookmark struct with the values. If the JSON is malformed, it returns a 400 error immediately.
- The validation check ensures both Title and URL are provided. Missing fields return a 400 with a helpful error message.
- strconv.Itoa(nextID) converts the integer counter to a string ID, then increments the counter for the next bookmark.
- w.WriteHeader(http.StatusCreated) sends a 201 status code, which is the standard HTTP response for "resource successfully created."
Register the handler on the mux
The handler function exists, but the server doesn't know to route POST requests to it yet. You need to register it on the mux just like the other two handlers.
- In the main function, find this line:
mux.HandleFunc("GET /bookmarks/{id}", handleGetBookmark)
- Add this line directly below it:
mux.HandleFunc("POST /bookmarks", handleCreateBookmark)
How does method-based routing work?
The pattern "POST /bookmarks" tells the mux to only match POST requests to /bookmarks. A GET request to the same path goes to handleListBookmarks instead. Go's ServeMux handles this routing for you based on the method prefix in the pattern.
- Save main.go.
Test the create endpoint
Time to send a POST request with a JSON body and confirm the new bookmark gets stored.
- In your terminal, stop the running server by pressing Ctrl+C.
- Restart the server with the updated code by running:
go run main.go
- Open a second terminal window.
- Create a new bookmark by running this curl command:
curl -X POST http://localhost:8080/bookmarks -H "Content-Type: application/json" -d '{"title":"GitHub","url":"https://github.com","tags":["code","git"]}'
You should see the newly created bookmark returned as JSON with an assigned ID of "3".
- Verify the bookmark was stored by listing all bookmarks:
curl http://localhost:8080/bookmarks
You should see all three bookmarks in the JSON response, including the new GitHub bookmark you just created.
- Test validation by sending a request with a missing title:
curl -X POST http://localhost:8080/bookmarks -H "Content-Type: application/json" -d '{"url":"https://example.com"}'
You should see the error message title and url are required confirming your validation works.
Seeing "invalid JSON" or a connection error?
- Make sure you stopped the old server and restarted it with go run main.go after saving your changes.
- Check that the single quotes around the JSON body are correct. On Windows, you may need to use double quotes and escape the inner quotes.
- Confirm that "strconv" is in your import block. A missing import causes a compile error.
Why is my POST request returning an error?
✔️ Awesome, I've got everything!
Great. Double check you've saved main.go and that all three endpoints work before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.go file at this point:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
type Bookmark struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Tags []string `json:"tags"`
}
var (
bookmarks = []Bookmark{
{ID: "1", Title: "Go Documentation", URL: "https://go.dev/doc", Tags: []string{"go", "docs"}},
{ID: "2", Title: "Go Blog", URL: "https://go.dev/blog", Tags: []string{"go", "blog"}},
}
nextID = 3
mu sync.Mutex
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /bookmarks", handleListBookmarks)
mux.HandleFunc("GET /bookmarks/{id}", handleGetBookmark)
mux.HandleFunc("POST /bookmarks", handleCreateBookmark)
fmt.Println("Bookmarks API server starting on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleListBookmarks(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(bookmarks)
}
func handleGetBookmark(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
id := r.PathValue("id")
for _, b := range bookmarks {
if b.ID == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(b)
return
}
}
http.Error(w, "bookmark not found", http.StatusNotFound)
}
func handleCreateBookmark(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
var b Bookmark
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if b.Title == "" || b.URL == "" {
http.Error(w, "title and url are required", http.StatusBadRequest)
return
}
b.ID = strconv.Itoa(nextID)
nextID++
bookmarks = append(bookmarks, b)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(b)
}
Your API now handles creating, listing, and retrieving bookmarks. Next up, you'll compile it into a standalone binary you can run anywhere.
Build and Run Your Binary
Your API has all three endpoints working. You've tested listing, getting, and creating bookmarks using curl. Now it's time to take your Go code and turn it into something you can share or deploy.
Go compiles your source code into a single standalone binary. That binary runs on its own without needing Go installed on the machine. This is one of Go's biggest advantages for deployment.
In this step, get ready to:
- Compile your Go code into a standalone binary.
- Run the compiled binary and test all endpoints one final time.
Stop the server and compile your binary
Up until now, you've been running your server with go run main.go. That compiles and executes in one step, but it doesn't leave behind a permanent file you can distribute.
- Switch to the terminal where your server is running.
- Press Ctrl+C to stop the server.
You should see the terminal return to the command prompt with no error message.
- Compile your code into a standalone binary by running this command:
go build -o bookmarks-api
What does this command do?
The go build command compiles your Go source code into machine code. The -o bookmarks-api flag names the output file.
The result is a single executable binary that contains everything needed to run your server. No Go installation required on the target machine.
- Confirm the binary was created by running:
ls
You should see bookmarks-api listed alongside your main.go and go.mod files.
Don't see the binary?
Make sure you're in the bookmarks-api folder (the one containing main.go). The build command looks for Go files in the current directory.
If you see an error about syntax or undefined references, double-check that main.go is saved with no typos.
Help me troubleshoot my Go build error.
Run the binary and test your endpoints
Your compiled binary is a standalone program. Running it is just like running any other executable on your system.
- Start your server from the compiled binary by running:
./bookmarks-api
You should see Bookmarks API server starting on http://localhost:8080 printed in the terminal.
What's different from go run?
With go run main.go, Go compiles your code into a temporary binary, runs it, then discards it. With ./bookmarks-api, you're running the permanent binary you already compiled. It starts faster because there's no compilation step.
- Open a second terminal tab or window.
- Test the list endpoint by running:
curl http://localhost:8080/bookmarks
You should see your two seed bookmarks returned as a JSON array.
- Test the get-by-ID endpoint by running:
curl http://localhost:8080/bookmarks/1
You should see just the Go Documentation bookmark returned as a JSON object.
- Test the create endpoint by running:
curl -X POST http://localhost:8080/bookmarks -H "Content-Type: application/json" -d '{"title": "Go by Example", "url": "https://gobyexample.com", "tags": ["go", "tutorial"]}'
You should see the new bookmark returned with "id": "3" and a 201 status.
Seeing "connection refused"?
Make sure you ran ./bookmarks-api in the first terminal and it printed the startup message. The server must be running before you send requests.
If port 8080 is already in use from a previous run, stop the old process first with Ctrl+C in the terminal where it was running.
Help me fix a connection refused error when testing my Go API.
Secret mission
Add DELETE and Search Functionality
Your API can create and read bookmarks, but what about removing them? And what if you have hundreds of bookmarks and need to find the ones tagged 'go'? Extend your API with a DELETE endpoint and a tag-based search feature.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The bookmarks-api project folder on your Desktop (contains main.go, go.mod, and the compiled binary).
- Go 1.26 installed on your system.
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment with your API further.
- Your bookmarks-api folder stays on your Desktop, ready for you to pick up where you left off.
- Go remains installed and available for future projects.
Pause - I'll come back to this later
Stop any running processes to free up system resources. Your project files stay intact.
- If your server is still running in the terminal, press Ctrl+C to stop it.
- Your bookmarks-api folder and Go installation remain on your machine for when you return.
Delete - I don't want to use this again
Remove all project files and optionally uninstall Go from your system.
- If your server is still running, press Ctrl+C to stop it.
- Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/bookmarks-api
On Windows, run this in PowerShell instead:
Remove-Item -Recurse -Force ~\Desktop\bookmarks-api
To uninstall Go from your system:
- On macOS, delete the Go directory and the paths file by running:
sudo rm -rf /usr/local/go
sudo rm -f /etc/paths.d/go
- On Windows, open Control Panel, go to Add/Remove Programs, select Go Programming Language, and click Uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional REST API in Go using nothing but the standard library.
You've learned how to:
- Built a complete HTTP server with pattern-based routing, structs, and JSON tags for clean API responses.
- Handled JSON encoding and decoding for both incoming requests and outgoing responses.
- Compiled your code into a standalone binary you can run anywhere on your machine.
- Added DELETE and search-by-tag functionality to extend your API beyond basic CRUD.
Ready to quiz yourself?