diff --git a/internal/frontend/api/api.go b/internal/frontend/api/api.go
new file mode 100644
index 0000000..7fd3b07
--- /dev/null
+++ b/internal/frontend/api/api.go
@@ -0,0 +1,199 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package api
+
+import "time"
+
+// PathInfo holds information about a package or module path.
+type PathInfo struct {
+ Path string
+ ModulePath string
+ Version string
+ Kind string
+}
+
+// ////////////// ModuleInfo
+type ModuleInfoRequest struct {
+ Path string // the module path
+ Version string // the module version, or "latest"
+ Details *ModuleDetailsRequest // if non-nil, ModuleInfo.Details will be populated
+ Licenses *LicensesRequest // if non-nil, ModuleInfo.Licenses will be populated
+ Packages *PackagesRequest // if non-nil, ModuleInfo.Packages will be populated
+}
+
+type ModuleInfo struct {
+ Path string // same as ModuleInfoRequest.Path
+ Version string // same as ModuleInfoRequest.Version, or the resolved latest version
+ Details *ModuleDetails `json:",omitempty"`
+ Licenses *Licenses `json:",omitempty"`
+ Packages *Packages `json:",omitempty"`
+}
+
+type ModuleDetailsRequest struct {
+ WithReadme bool // if true, ModuleDetails.ReadmeContents will be populated
+}
+
+// ModuleDetails describes module-level details.
+type ModuleDetails struct {
+ CommitTime time.Time // as returned by the proxy’s .info endpoint
+ ReadmeFilePath string // path of README, relative to module root
+ ReadmeContents []byte // raw contents, if includeReadme is true
+ HasGoMod bool // whether the module has a go.mod file
+ SourceInfo *SourceInfo // can be nil
+ IsRedistributable bool
+}
+
+// SourceInfo provides links to the module's source code, if they can be deduced.
+type SourceInfo struct {
+ RepoURL string // URL of repo containing module
+ ModuleURL string // URL of module directory; append subdir to get subdir URL
+ CommitID string // tag or ID of commit (revision) corresponding to version
+}
+
+type LicensesRequest struct {
+ WithContents bool // if true, License.Contents wil be populated
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// Licenses is a list of licenses.
+type Licenses struct {
+ Items []*APILicense // no more than LicensesRequest.Max items
+ Total int // total number of items, disregarding Max and From
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// APILicense describes a license file
+type APILicense struct {
+ FilePath string // path to license file from module root
+ Types []string // license types, as determined by our detector
+ Contents []byte `json:",omitempty"` // the file’s bytes, if WithContents is true
+}
+
+// PackagesRequest should be passed as part of a ModuleInfoRequest to obtain package information.
+type PackagesRequest struct {
+ // RelativePath is relative to the module path.
+ // If empty, return all packages in the module.
+ // If RelativePath ends in "/", return all packages in the module with RelativePath as a prefix.
+ // Otherwise, return the single package whose import path is modulePath/RelativePath.
+ RelativePath string
+ Details *PackageDetailsRequest // if non-nil, Package.Details will be populated
+ Licenses *LicensesRequest // if non-nil, Package.Licenses will be populated
+ Imports *ImportsRequest // if non-nil, Package.Imports will be populated
+ Importers *ImportersRequest // if non-nil, Package.Importers will be populated
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// Packages is a list of packages.
+type Packages struct {
+ Items []*Package // no more than PackagesRequest.Max items
+ Total int // total number of items, disregarding Max and From
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// Package describes a Go package.
+type Package struct {
+ Path string // import path
+ Details *PackageDetails `json:",omitempty"`
+ Licenses *Licenses `json:",omitempty"`
+ Imports *Imports `json:",omitempty"`
+ Importers *Importers `json:",omitempty"`
+}
+
+// PackageDetailsRequest should be passed as part of a PackagesRequest to obtain package details.
+type PackageDetailsRequest struct {
+ WithDocHTML bool // if true, PackageDetails.DocHTML will be populated
+}
+
+// PackageDetails decribes package-level details.
+type PackageDetails struct {
+ Name string // as distinct from its import path
+ Synopsis string // from the doc
+ DocHTML []byte `json:",omitempty"` // if WithDocHTML is true
+ GOOS string // value of GOOS used to process this package
+ GOARCH string // value of GOARCH used to process this package
+ IsRedistributable bool
+}
+
+// ImportsRequest should be passed as part of a PackagesRequest to obtain the package's imports.
+type ImportsRequest struct {
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// Imports is a list of the imports of a package.
+type Imports struct {
+ Items []PathInfo // Kind is always "package". ModulePath and Version may be empty.
+ Total int // total number of items, disregarding Max and From
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// ImportersRequest should be passed as part of a PackagesRequest to obtain the list of packages
+// that import a given package.
+type ImportersRequest struct {
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// Importers is a list of the importers of a package.
+type Importers struct {
+ Items []PathInfo // Kind is always "package". ModulePath and Version may be empty.
+ Total int // total number of items, disregarding Max and From; may be -1 if the total cannot be determined
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// ////////////// MajorVersions
+type MajorVersionsRequest struct {
+ // The path of the module whose versions should be listed, or the module containing the
+ // package whose versions should be listed (if PackagePath is non-empty).
+ // The "/vN" suffix is ignored; the list will always include all versions of all "/vN" variants.
+ ModulePath string
+ // If non-empty, only versions of the module that contain this import path will be returned.
+ PackagePath string
+ ExcludePseudo bool // if true, pseudo-versions are not returned
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// MajorVersions is a list of version information.
+// It is the result of the Versions method.
+type MajorVersions struct {
+ Versions []MajorVersionInfo // ordered from highest to lowest
+ Total int // total number of items, disregarding Max and From
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// MajorVersionInfo holds information about a module version.
+type MajorVersionInfo struct {
+ ModulePath string
+ Version string
+ CommitTime time.Time // as returned by the proxy’s .info endpoint
+}
+
+// ////////////// Search
+type SearchRequest struct {
+ Query string
+ Symbol string `json:"Symbol"`
+ Offset int `json:"Offset"`
+ SearchSymbols bool `json:"SearchSymbols"`
+ Max int // maximum number of items to return; if <0, all are returned
+ From string // first item to return; must come from a previous response's Next field
+}
+
+// SearchResults is a list of search results.
+// It is the result of the Search method.
+type SearchResults struct {
+ Results []*SearchResult
+ Total int // total number of items, disregarding Max and From
+ Next string // if non-empty, more are available; set to From in request to retrieve
+}
+
+// The SearchResult should be a mirror of the results returned in pkg.go.dev search
+type SearchResult struct {
+ Path string // module path
+ Synopsis string // short summary of the module details
+ Version string // latest module version
+}
diff --git a/internal/frontend/api/handlers.go b/internal/frontend/api/handlers.go
new file mode 100644
index 0000000..bcfc911
--- /dev/null
+++ b/internal/frontend/api/handlers.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/frontend/serrors"
+)
+
+func HandleSearch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) error {
+ var sr SearchRequest
+ if err := json.NewDecoder(r.Body).Decode(&sr); err != nil {
+ return &serrors.ServerError{Status: http.StatusBadRequest, ResponseText: "Invalid search payload"}
+ }
+ mockResults := &SearchResults{
+ Results: []*APISearchResult{
+ {Path: "example.com/module", Synopsis: "An example module", Version: "v1.2.3"},
+ {Path: "example.com/another/module", Synopsis: "Another example module", Version: "v2.0.0"},
+ },
+ Total: 2,
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(mockResults); err != nil {
+ return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
+ }
+ return nil
+}
+
+func HandleModuleInfo(w http.ResponseWriter, r *http.Request, ds internal.DataSource) error {
+ var mir ModuleInfoRequest
+ if err := json.NewDecoder(r.Body).Decode(&mir); err != nil {
+ return &serrors.ServerError{Status: http.StatusBadRequest, ResponseText: "Invalid moduleinfo payload"}
+ }
+ mockInfo := &ModuleInfo{
+ Path: mir.Path,
+ Version: mir.Version,
+ Details: &ModuleDetails{
+ CommitTime: time.Now(),
+ ReadmeFilePath: "README.md",
+ HasGoMod: true,
+ },
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(mockInfo); err != nil {
+ return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
+ }
+ return nil
+}
+
+func HandleMajorVersions(w http.ResponseWriter, r *http.Request, ds internal.DataSource) error {
+ var mvr MajorVersionsRequest
+ if err := json.NewDecoder(r.Body).Decode(&mvr); err != nil {
+ return &serrors.ServerError{Status: http.StatusBadRequest, ResponseText: "Invalid majorversions payload"}
+ }
+ mockVersions := &MajorVersions{
+ Versions: []MajorVersionInfo{
+ {ModulePath: mvr.ModulePath, Version: "v2.1.0", CommitTime: time.Now()},
+ {ModulePath: mvr.ModulePath, Version: "v1.5.2", CommitTime: time.Now().Add(-24 * time.Hour)},
+ },
+ Total: 2,
+ }
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(mockVersions); err != nil {
+ return &serrors.ServerError{Status: http.StatusInternalServerError, ResponseText: "Internal server error"}
+ }
+ return nil
+}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 5f621fd..f7428c8 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -24,6 +24,7 @@
"golang.org/x/pkgsite/internal/config"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/experiment"
+ "golang.org/x/pkgsite/internal/frontend/api"
pagepkg "golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
"golang.org/x/pkgsite/internal/frontend/templates"
@@ -205,6 +206,9 @@
handle("GET /search-help", s.staticPageHandler("search-help", "Search Help"))
handle("GET /license-policy", s.licensePolicyHandler())
handle("GET /about", s.staticPageHandler("about", "About"))
+ handle("POST /api/search", s.errorHandler(api.HandleSearch))
+ handle("POST /api/moduleinfo", s.errorHandler(api.HandleModuleInfo))
+ handle("POST /api/majorversions", s.errorHandler(api.HandleMajorVersions))
handle("GET /badge/", http.HandlerFunc(s.badgeHandler))
handle("GET /C", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Package "C" is a special case: redirect to /cmd/cgo.