reqflow

package module
v1.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 11 Imported by: 0

README

reqflow

Go Reference Go Report Card License

Trace any HTTP request through your Go codebase, statically.

One command. No instrumentation. No runtime. Just point it at your code.

go install github.com/ShipOrBleed/reqflow/cmd/reqflow@latest

Why

You just joined a team. There's a bug in POST /orders. Where do you even start?

You grep for the route, find the handler, cmd-click into the service, cmd-click again into the store, read struct tags to figure out the database table. You do this every time, for every repo, for every bug.

reqflow does all of that in one command.


Quick Start

cd your-go-project/
reqflow trace "/orders" ./...

That's it. reqflow parses your code, finds every route registered on /orders, lets you pick one, and shows the complete request path with exact method names and file locations.


Commands

reqflow trace — Trace a request path

Just type the path. reqflow finds all HTTP methods registered on it and lets you pick:

$ reqflow trace "/orders" ./...

Multiple routes match "/orders":

  1.  GET /orders
  2.  POST /orders

Enter number (1-2): 2

POST /orders
──────────────

  [H]  OrderHandler   internal/handler/orders.go:45
       CreateOrder()
         → svc.Create()

  │
  ↓  delegates to
  │
  [S]  orderService   internal/service/orders.go:89
       Create()
         → store.Insert()

  │
  ↓  queries via
  │
  [D]  OrderStore     internal/store/orders.go:67
       Insert()

Other ways to trace:

reqflow trace "POST /orders" ./...    # exact route
reqflow trace "budget" ./...          # substring — shows all matches
reqflow routes — List all routes in a service
$ reqflow routes ./...

  GET     /orgs/{orgID}/budgets              Handler.GetBudgets()        handler/handler.go:350
  POST    /orgs/{orgID}/budgets              Handler.CreateBudget()      handler/handler.go:398
  DELETE  /orgs/{orgID}/budgets/{budgetID}   Handler.DeleteBudget()      handler/handler.go:442
  GET     /orgs/{orgID}/reports/metrics      Handler.GetMetrics()        handler/handler.go:700
  ...

39 routes across 1 handlers

Export as JSON:

reqflow routes -format json ./...

What You Get

Each node in the trace shows:

Part Meaning
[H] [S] [D] [C] [M] Layer — Handler, Service, Store, Client, Model
Handler Struct name
internal/handler/handler.go:700 Package, file, and line of the method (not the struct)
GetMetrics() The specific method that handles this route
→ svc.GetMetricsByOrgPaginated() What that method calls on the next layer

Real-World Examples

Handler → Service → Store (most common)
$ reqflow trace "GET /orgs/{orgID}/budgets" ./...

  [H]  Handler   internal/handler/handler.go:350
       GetBudgets()
         → svc.GetBudgets()

  [S]  service   internal/service/service.go:4258
       GetBudgets()
         → store.GetBudgetsByResourceUIDs()
         → store.GetBudgetsByResourceGroupIDs()
         → store.GetBudgets()

  [D]  Store     internal/store/store.go:32
       GetBudgetsByResourceUIDs()
       GetBudgetsByResourceGroupIDs()
       GetBudgets()
Handler → External Clients (gRPC/HTTP)
$ reqflow trace "POST /orgs/{orgID}/actions" ./...

  [H]  Handler            internal/handler.go:80
       ManualAction()
         → cloudAccountFetcher.GetPermissionLevel()
         → resourceFetcher.GetGroupForResource()

  [C]  ConfigGRPCClient   internal/config_grpc.go:18
       GetPermissionLevel()
         → client.GetCloudAccountPermission()
       GetGroupForResource()

  [C]  ConfigServiceClient   internal/configclient/config_grpc.pb.go:35
       GetCloudAccountPermission()
Interactive route selection
$ reqflow trace "budgets" ./...

Multiple routes match "budgets":

  1.  GET /orgs/{orgID}/budgets/summary
  2.  GET /orgs/{orgID}/budgets
  3.  POST /orgs/{orgID}/budgets
  4.  PUT /orgs/{orgID}/budgets/{budgetID}
  5.  DELETE /orgs/{orgID}/budgets/{budgetID}

Enter number (1-5): 3

Pick a number, see the trace. One session, no re-running.


Flags

reqflow trace
reqflow trace [flags] <route> [packages]
Flag Description
-format text Terminal output (default)
-format html Self-contained HTML page
-out <file> Write to file instead of stdout
-tablemap Show database tables mapped from model struct tags
-envmap Show environment variables read via os.Getenv / viper
reqflow trace -format html -out trace.html "POST /orders" ./...
reqflow trace -tablemap "GET /users/{id}" ./...
reqflow trace -envmap "POST /orders" ./...
reqflow routes
reqflow routes [flags] [packages]
Flag Description
-format text Aligned table output (default)
-format json JSON array
-out <file> Write to file instead of stdout

How It Works

reqflow uses Go's type system — not grep, not regexes.

  1. Loads packages with golang.org/x/tools/go/packages and walks the AST
  2. Classifies structs by structure — a store is any struct holding a *sql.DB field; a handler is any struct whose methods accept *gofr.Context or *gin.Context; an HTTP client is any struct named *Client
  3. Extracts routes from app.GET("/path", h.Method) calls — including inline anonymous handlers like app.GET("/health", func(ctx) { ... })
  4. Builds a method-level call index — knows Handler.GetMetrics() calls svc.GetMetricsByOrgPaginated(), not just "handler depends on service"
  5. Traces the precise path — follows actual method calls, not struct dependencies. Only shows the nodes and methods touched by this specific request

Supported Frameworks

Framework Handler signature
GoFr func(ctx *gofr.Context) (any, error)
Gin func(c *gin.Context)
Echo func(c echo.Context) error
Fiber func(c *fiber.Ctx) error
net/http func(w http.ResponseWriter, r *http.Request)
Layer classification
Layer Badge Name patterns Structural detection
Handler [H] *Handler, *Controller, *Endpoint Methods accept framework context
Service [S] *Service, *UseCase, *Manager
Store [D] *Store, *Repository, *Repo Has DB client field (*sql.DB, *gorm.DB, etc.)
Client [C] *Client, *Caller, *Connector
Model [M] *Model, *Entity, *DTO Has DB struct tags

Override with .reqflow.yml:

layers:
  service_pattern: ".*Service$|.*Processor$"
  store_pattern:   ".*Store$|.*Repository$"
  model_pattern:   ".*Model$|.*Entity$"

Configuration

Optional .reqflow.yml in your project root:

parser:
  ignore_packages:
    - vendor
    - _test
    - mock

layers:
  service_pattern: ".*Service$"
  store_pattern:   ".*Store$|.*Repository$|.*Repo$"
  model_pattern:   ".*Model$|.*Entity$"

Use as a Go library

import "github.com/ShipOrBleed/reqflow"

// Parse codebase
graph, err := reqflow.Parse(reqflow.ParseOptions{Dir: "."})

// List all routes
routes := reqflow.ListRoutes(graph)
for _, r := range routes {
    fmt.Printf("%s %s → %s.%s()\n", r.Method, r.Path, r.HandlerName, r.MethodName)
}

// Trace a specific route
result := reqflow.Trace("POST /orders", graph)
fmt.Println(result.Route)
for _, node := range result.Chain {
    fmt.Printf("[%s] %s\n", node.Kind, node.Name)
}

License

MIT — see LICENSE

Documentation

Overview

Package reqflow statically traces HTTP request paths through Go codebases.

Given a route like "POST /orders", reqflow finds the handler, follows the call chain through services and stores, and shows exactly which methods are called at each layer — with file names and line numbers.

Install the CLI

go install github.com/ShipOrBleed/reqflow/cmd/reqflow@latest

Trace a request

graph, err := reqflow.Parse(reqflow.ParseOptions{Dir: "."})
if err != nil {
    log.Fatal(err)
}
result := reqflow.Trace("POST /orders", graph)
for _, node := range result.Chain {
    fmt.Printf("[%s] %s\n", node.Kind, node.Name)
}

List all routes

routes := reqflow.ListRoutes(graph)
for _, r := range routes {
    fmt.Printf("%s %s → %s.%s()\n", r.Method, r.Path, r.HandlerName, r.MethodName)
}

Supported frameworks

GoFr, Gin, Echo, Fiber, and net/http handlers are automatically detected. Stores are detected by struct field types (*sql.DB, *gorm.DB, *mongo.Client, etc.), not by naming conventions.

Index

Constants

This section is empty.

Variables

View Source
var Version = "dev"

Version is set at build time via ldflags.

Functions

func EdgeLabel

func EdgeLabel(from, to *Node) string

EdgeLabel returns a human-readable description of the transition between two adjacent steps in the trace chain.

func ExtractEnvMap

func ExtractEnvMap(pkgs []*packages.Package, graph *Graph)

ExtractEnvMap scans AST for environment variable reads via os.Getenv, os.LookupEnv, and viper.Get* calls. Creates KindEnvVar nodes and EdgeReads edges linking the consuming function/struct to the env var.

func ExtractGRPC

func ExtractGRPC(pkgs []*packages.Package, graph *Graph)

ExtractGRPC detects gRPC service registrations and unimplemented server embeds.

func ExtractGoModDeps

func ExtractGoModDeps(dir string, graph *Graph)

ExtractGoModDeps parses go.mod to identify cloud/infra dependencies.

func ExtractMiddleware

func ExtractMiddleware(pkgs []*packages.Package, graph *Graph)

ExtractMiddleware scans for .Use() calls to detect middleware registrations.

func ExtractTableMap

func ExtractTableMap(pkgs []*packages.Package, graph *Graph)

ExtractTableMap inspects KindModel nodes for GORM/sqlx/bson struct tags and creates KindTable nodes representing the inferred database tables, linked via EdgeMapsTo edges.

func FormatRoutesJSON

func FormatRoutesJSON(routes []RouteInfo) string

FormatRoutesJSON renders route info as a JSON array of objects. Each object has fields: method, path, handler, method_name, file, line.

func FormatRoutesText

func FormatRoutesText(routes []RouteInfo) string

FormatRoutesText renders route info as an aligned text table suitable for terminal output. Includes a summary line showing total routes and handler count.

func Warn

func Warn(format string, args ...any)

Warn prints a warning message to stderr. Used by analysis functions that encounter non-fatal errors (e.g., git not available, file not found).

Types

type Edge

type Edge struct {
	From, To string
	Kind     EdgeKind
}

Edge represents a directed relationship between two nodes in the graph.

type EdgeKind

type EdgeKind string

EdgeKind classifies the relationship between two nodes.

const (
	EdgeEmbeds     EdgeKind = "embeds"
	EdgeImplements EdgeKind = "implements"
	EdgeDepends    EdgeKind = "depends"
	EdgeCalls      EdgeKind = "calls"      // function-to-function call
	EdgeFlows      EdgeKind = "flows"      // request data flow (handler→service→store)
	EdgeReads      EdgeKind = "reads"      // reads env var
	EdgeMapsTo     EdgeKind = "maps_to"    // model→table mapping
	EdgeTransitive EdgeKind = "transitive" // transitive dependency
	EdgePublishes  EdgeKind = "publishes"  // event publish
	EdgeSubscribes EdgeKind = "subscribes" // event subscribe
	EdgeRPC        EdgeKind = "rpc"        // cross-service RPC call
)

type Field

type Field struct{ Name, Type, Tag string }

Field represents a struct field with its name, type, and struct tag.

type Graph

type Graph struct {
	Nodes       map[string]*Node
	Edges       []Edge
	Clusters    map[string][]string // pkg path → []node IDs
	Meta        map[string]string   // graph-level metadata (repo name, commit SHA, etc.)
	MethodCalls MethodCallIndex     // method-level call index for trace precision
}

Graph is the core data structure representing the architecture of a Go codebase. It contains nodes (components), edges (relationships), and clusters (package groupings).

func NewGraph

func NewGraph() *Graph

NewGraph creates an empty, initialized Graph ready for use.

func Parse

func Parse(opts ParseOptions) (*Graph, error)

Parse loads Go packages from the given directory, walks the AST, and builds a complete architecture graph. It detects handlers, services, stores, models, routes, middleware, gRPC registrations, events, and struct field dependencies.

The resulting Graph can be passed to Trace to follow a specific request path, or to ListRoutes to enumerate all registered HTTP routes.

Example:

graph, err := reqflow.Parse(reqflow.ParseOptions{Dir: "./..."})
if err != nil {
    log.Fatal(err)
}
result := reqflow.Trace("POST /orders", graph)

func (*Graph) AddEdge

func (g *Graph) AddEdge(from, to string, kind EdgeKind)

AddEdge adds a directed edge between two nodes.

func (*Graph) AddNode

func (g *Graph) AddNode(n *Node)

AddNode adds a node to the graph, initializing its Meta map if nil, and registering it in the appropriate package cluster.

type MethodCall

type MethodCall struct {
	FieldName    string // e.g. "svc", "store"
	TargetMethod string // e.g. "GetMetrics", "GetCostRecords"
}

MethodCall represents a call from one struct method to another struct's method via a struct field. For example, if Handler.GetMetrics() contains the call h.svc.GetMetricsByOrg(), the MethodCall would be {FieldName: "svc", TargetMethod: "GetMetricsByOrg"}.

type MethodCallIndex

type MethodCallIndex map[string][]MethodCall

MethodCallIndex maps "pkg.Struct.Method" → list of outgoing method calls.

type Node

type Node struct {
	ID      string // "pkg/path.TypeName"
	Kind    NodeKind
	Name    string
	Package string
	Fields  []Field
	Methods []string
	File    string
	Line    int
	Meta    map[string]string // e.g. "route": "GET /users"
}

Node represents a single component in the architecture graph (struct, interface, function, handler, service, store, model, etc.). Nodes are uniquely identified by their ID (typically "package.TypeName") and carry metadata in the Meta map.

type NodeKind

type NodeKind string

NodeKind classifies a graph node into an architectural layer.

const (
	KindStruct     NodeKind = "struct"
	KindInterface  NodeKind = "interface"
	KindFunc       NodeKind = "func"
	KindHandler    NodeKind = "handler"    // HTTP handler
	KindStore      NodeKind = "store"      // DB layer / Repository
	KindClient     NodeKind = "client"     // External HTTP/gRPC client
	KindModel      NodeKind = "model"      // DB entity / Model
	KindService    NodeKind = "service"    // Business logic
	KindEvent      NodeKind = "event"      // Event Bus (Kafka/Rabbit)
	KindMiddleware NodeKind = "middleware" // HTTP Middleware
	KindGRPC       NodeKind = "grpc"       // gRPC service
	KindInfra      NodeKind = "infra"      // External infrastructure
	KindRoute      NodeKind = "route"      // API endpoint
	KindEnvVar     NodeKind = "envvar"     // Environment variable
	KindTable      NodeKind = "table"      // Database table
	KindDep        NodeKind = "dependency" // go.mod transitive dependency
	KindContainer  NodeKind = "container"  // Docker/K8s container
	KindProtoRPC   NodeKind = "proto_rpc"  // Proto RPC method
	KindProtoMsg   NodeKind = "proto_msg"  // Proto message type
)

type ParseOptions

type ParseOptions struct {
	// Dir is the Go module directory to analyze. Use "./..." or "." for current directory.
	Dir string

	// Filter limits analysis to packages matching this substring.
	Filter string

	// Config is an optional .reqflow.yml configuration. See [LoadConfig].
	Config *ReqflowConfig

	// EnvMap enables environment variable detection (os.Getenv, viper.Get*).
	// Creates KindEnvVar nodes and EdgeReads edges.
	EnvMap bool

	// TableMap enables model-to-database table mapping from struct tags (gorm, sqlx, bson).
	// Creates KindTable nodes and EdgeMapsTo edges.
	TableMap bool
}

ParseOptions configures the Parse function.

type ReqflowConfig

type ReqflowConfig struct {
	Linter struct {
		VetRules []string `yaml:"vet_rules"`
	} `yaml:"linter"`
	Parser struct {
		IgnorePackages []string `yaml:"ignore_packages"`
		DomainNaming   struct {
			ServiceMatch string `yaml:"service_match"`
			StoreMatch   string `yaml:"store_match"`
			ModelMatch   string `yaml:"model_match"`
		} `yaml:"domain_naming"`
	} `yaml:"parser"`
	Thresholds struct {
		MaxCycles         *int `yaml:"max_cycles"`
		MaxOrphans        *int `yaml:"max_orphans"`
		MaxSecurityIssues *int `yaml:"max_security_issues"`
	} `yaml:"thresholds"`

	// Compiled regexes (not from YAML)
	ServiceRegex *regexp.Regexp `yaml:"-"`
	StoreRegex   *regexp.Regexp `yaml:"-"`
	ModelRegex   *regexp.Regexp `yaml:"-"`
}

ReqflowConfig represents the .reqflow.yml configuration file. Place this file in your project root to customize layer detection patterns and ignore specific packages.

Example .reqflow.yml:

parser:
  ignore_packages:
    - vendor
    - _test
  domain_naming:
    service_match: ".*Service$"
    store_match: ".*Store$|.*Repository$"

func LoadConfig

func LoadConfig(path string) (*ReqflowConfig, error)

LoadConfig reads and parses a .reqflow.yml configuration file. Returns an error if the file doesn't exist or contains invalid YAML. Regex patterns in domain_naming are compiled into ServiceRegex, StoreRegex, ModelRegex.

type RouteInfo

type RouteInfo struct {
	Method      string // HTTP method: GET, POST, PUT, DELETE, PATCH
	Path        string // URL path, e.g. "/orgs/{orgID}/budgets"
	HandlerName string // Handler struct name, e.g. "OrderHandler"
	MethodName  string // Handler method name, e.g. "GetBudgets"
	File        string // Absolute path to the source file
	Line        int    // Line number where the handler method is defined
}

RouteInfo holds information about a single registered HTTP route, including the handler struct, method name, and source location.

func ListRoutes

func ListRoutes(g *Graph) []RouteInfo

ListRoutes extracts all registered HTTP routes from the graph. Routes are sorted by path, then by HTTP method.

Example:

graph, _ := reqflow.Parse(reqflow.ParseOptions{Dir: "."})
routes := reqflow.ListRoutes(graph)
for _, r := range routes {
    fmt.Printf("%s %s → %s.%s()\n", r.Method, r.Path, r.HandlerName, r.MethodName)
}

type TraceResult

type TraceResult struct {
	// Route is the matched route string, e.g. "POST /orders".
	Route string

	// Handler is the handler node that owns this route.
	Handler *Node

	// Chain is the ordered list of nodes in the request path:
	// handler → service → store → model, sorted by architectural layer.
	Chain []*Node

	// Tables lists database table names touched by this request path.
	// Populated when -tablemap flag is used.
	Tables []string

	// EnvVars lists environment variable names read along this request path.
	// Populated when -envmap flag is used.
	EnvVars []string

	// NotFound is true when no handler matched the given route query.
	NotFound bool

	// CalledMethods maps node ID → list of specific methods called on that node
	// in this request path. Only methods actually invoked are included, not all
	// methods on the struct.
	CalledMethods map[string][]string

	// MethodCalls is the full method-level call index, used by renderers
	// to display sub-calls (e.g. → svc.GetMetrics()).
	MethodCalls MethodCallIndex

	// MultiMatch is populated when a path-only query matches multiple HTTP
	// methods (e.g. "/orders" → GET /orders, POST /orders). The caller should
	// present these options for the user to select one.
	MultiMatch []string
}

TraceResult holds the complete request path for a single HTTP route.

When a route is found, Chain contains the ordered list of nodes from handler through service to store, CalledMethods maps each node ID to the specific methods called on it, and MethodCalls provides the full call index for rendering sub-calls.

When multiple routes match a path-only query (e.g. "/orders" matches both GET and POST), MultiMatch is populated instead of Chain, allowing the caller to present a selection to the user.

func Trace

func Trace(route string, g *Graph) *TraceResult

Trace finds the handler matching route and returns the complete static request path through the codebase — handler → service → store → tables, plus any environment variables read along the way.

route may be:

  • exact match: "POST /orders"
  • path only: "/orders" (matches any HTTP method)
  • partial: "orders" (substring match against all registered routes)

Directories

Path Synopsis
cmd
reqflow command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL