A shortener converts a URL into a compact key for sharing, then resolves that key back to the original URL. A REST API exposes the two operations: shorten a submitted link and redirect a request for its key. section 8.1
Figure 8.1 The Shorten and Resolve handlers provide a REST API over HTTP using the Shortener service, which is backed by an in-memory database.
Requests enter HTTP handlers, which delegate shortening and resolution to core link logic backed initially by in-memory storage. section 8.1
The core service stores Link values by Key. Shorten validates a link, supplies a key when needed, rejects duplicate keys, and saves the result. Resolve validates a key, looks it up, and returns a not-found error when absent. section 8.2.3
A practical implementation emphasizes reliable boundaries:
- Keep core link logic independent of HTTP and storage packages, making it straightforward to test and reusable by multiple adapters. section 8.2
- Make the zero value usable by lazily initializing the links map before its first write; use a constructor only when zero-value support would add complexity. section 8.2.3
- Protect shared in-memory state for concurrent goroutines and use validation plus consistent wrapped errors for bad requests, conflicts, and missing links. section 8.2.3
- Use matching operation names across layers, such as Resolve in both the HTTP package and service, so request flow remains easy to follow. section 8.1
In-memory service
Declare a Shortener with no setup, call Shorten to store a valid link, then call Resolve with its key. The service initializes its map on first storage; a missing key returns a not-found error rather than an empty link. section 8.2.3
Storage can change without changing the service role. A SQLite-backed implementation inserts links with ExecContext and resolves them with QueryRowContext and Scan, translating missing rows into the same not-found error used by other implementations. section 10.0 section 10.2.5
Listing 10.10 Resolving links (link/sqlite/shortener.go)
// Resolve resolves a [link.Link] by its [link.Key] from the database.
func (s *Shortener) Resolve(
ctx context.Context, key link.Key,
) (link.Link, error) {
if key.Empty() {
return link.Link{}, fmt.Errorf(. . .)
}
if err := key.Validate(); err != nil {
return link.Link{}, fmt.Errorf(
"validating: %w: %w",
err, link.ErrBadRequest,
)
}
// Retrieve the link from the database.
var uri string
err := s.db.QueryRowContext( #1
ctx,
`SELECT uri FROM links WHERE short_key = $1`,
key,
).Scan(&uri) #2
if errors.Is(err, sql.ErrNoRows) { #3
return link.Link{}, link.ErrNotFound
}
if err != nil {
return link.Link{}, fmt.Errorf(
"retrieving: %w: %w",
err, link.ErrInternal,
)
}
return link.Link{Key: key, URL: uri}, nil
}
The SQLite resolver validates the key, selects the URL by short key, scans the row, and maps an absent row to a service-level not-found error. section 10.2.5
