Repository: goadesign/goa
Pull request #3616 (2024-11-26 21:13:14)

Initial interceptors implementation
This PR implements the interceptors proposal described in https://github.com/goadesign/goa/issues/3614


Patch:
diff --git a/codegen/cli/cli.go b/codegen/cli/cli.go
index a69f7470..6c852cc5 100644
--- a/codegen/cli/cli.go
+++ b/codegen/cli/cli.go
@@ -34,6 +34,8 @@ type (
 		// PkgName is the service HTTP client package import name,
 		// e.g. "storagec".
 		PkgName string
+		// Interceptors contains the data for client interceptors if any.
+		Interceptors *InterceptorData
 	}
 
 	// SubcommandData contains the data needed to render a sub-command.
@@ -58,6 +60,14 @@ type (
 		Example string
 	}
 
+	// InterceptorData contains the data needed to generate interceptor code.
+	InterceptorData struct {
+		// VarName is the name of the interceptor variable.
+		VarName string
+		// PkgName is the package name containing the interceptor type.
+		PkgName string
+	}
+
 	// FlagData contains the data needed to render a command-line flag.
 	FlagData struct {
 		// Name is the name of the flag, e.g. "list-vintage"
@@ -151,11 +161,21 @@ func BuildCommandData(data *service.Data) *CommandData {
 	if description == "" {
 		description = fmt.Sprintf("Make requests to the %q service", data.Name)
 	}
+
+	var interceptors *InterceptorData
+	if len(data.ClientInterceptors) > 0 {
+		interceptors = &InterceptorData{
+			VarName: "inter",
+			PkgName: data.PkgName,
+		}
+	}
+
 	return &CommandData{
-		Name:        codegen.KebabCase(data.Name),
-		VarName:     codegen.Goify(data.Name, false),
-		Description: description,
-		PkgName:     data.PkgName + "c",
+		Name:         codegen.KebabCase(data.Name),
+		VarName:      codegen.Goify(data.Name, false),
+		Description:  description,
+		PkgName:      data.PkgName + "c",
+		Interceptors: interceptors,
 	}
 }
 
@@ -682,7 +702,8 @@ const parseFlagsT = `var (
 `
 
 // input: commandData
-const commandUsageT = `{{ printf "%sUsage displays the usage of the %s command and its subcommands." .Name .Name | comment }}
+const commandUsageT = `
+{{ printf "%sUsage displays the usage of the %s command and its subcommands." .VarName .Name | comment }}
 func {{ .VarName }}Usage() {
 	fmt.Fprintf(os.Stderr, ` + "`" + `{{ printDescription .Description }}
 Usage:
diff --git a/codegen/example/example_server.go b/codegen/example/example_server.go
index f896106e..35576abf 100644
--- a/codegen/example/example_server.go
+++ b/codegen/example/example_server.go
@@ -50,14 +50,17 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c
 	// Iterate through services listed in the server expression.
 	svcData := make([]*service.Data, len(svr.Services))
 	scope := codegen.NewNameScope()
+	hasInterceptors := false
 	for i, svc := range svr.Services {
 		sd := service.Services.Get(svc)
 		svcData[i] = sd
 		specs = append(specs, &codegen.ImportSpec{
 			Path: path.Join(genpkg, sd.PathName),
-			Name: scope.Unique(sd.PkgName),
+			Name: scope.Unique(sd.PkgName, "svc"),
 		})
+		hasInterceptors = hasInterceptors || len(sd.ServerInterceptors) > 0
 	}
+	interPkg := scope.Unique("interceptors", "ex")
 
 	var (
 		rootPath string
@@ -73,6 +76,9 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c
 		apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
 	}
 	specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg})
+	if hasInterceptors {
+		specs = append(specs, &codegen.ImportSpec{Path: path.Join(rootPath, "interceptors"), Name: interPkg})
+	}
 
 	sections := []*codegen.SectionTemplate{
 		codegen.Header("", "main", specs),
@@ -101,6 +107,18 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *c
 			FuncMap: map[string]any{
 				"mustInitServices": mustInitServices,
 			},
+		}, {
+			Name:   "server-main-interceptors",
+			Source: readTemplate("server_interceptors"),
+			Data: map[string]any{
+				"APIPkg":          apiPkg,
+				"InterPkg":        interPkg,
+				"Services":        svcData,
+				"HasInterceptors": hasInterceptors,
+			},
+			FuncMap: map[string]any{
+				"mustInitServices": mustInitServices,
+			},
 		}, {
 			Name:   "server-main-endpoints",
 			Source: readTemplate("server_endpoints"),
diff --git a/codegen/example/templates/server_endpoints.go.tpl b/codegen/example/templates/server_endpoints.go.tpl
index 3916ab3b..97be6436 100644
--- a/codegen/example/templates/server_endpoints.go.tpl
+++ b/codegen/example/templates/server_endpoints.go.tpl
@@ -11,7 +11,7 @@
 	{
 	{{- range .Services }}
 		{{- if .Methods }}
-			{{ .VarName }}Endpoints = {{ .PkgName }}.NewEndpoints({{ .VarName }}Svc)
+			{{ .VarName }}Endpoints = {{ .PkgName }}.NewEndpoints({{ .VarName }}Svc{{ if .ServerInterceptors }}, {{ .VarName }}Interceptors{{ end }})
 			{{ .VarName }}Endpoints.Use(debug.LogPayloads())
 			{{ .VarName }}Endpoints.Use(log.Endpoint)
 		{{- end }}
diff --git a/codegen/example/templates/server_interceptors.go.tpl b/codegen/example/templates/server_interceptors.go.tpl
new file mode 100644
index 00000000..c2b09fca
--- /dev/null
+++ b/codegen/example/templates/server_interceptors.go.tpl
@@ -0,0 +1,19 @@
+{{- if mustInitServices .Services }}
+	{{- if .HasInterceptors }}
+	{{ comment "Initialize the interceptors." }}
+	var (
+		{{- range .Services }}
+			{{- if and .Methods .ServerInterceptors }}
+		{{ .VarName }}Interceptors {{ .PkgName }}.ServerInterceptors
+			{{- end }}
+		{{- end }}
+	)
+	{
+	{{- range .Services }}
+		{{- if and .Methods .ServerInterceptors }}
+		{{ .VarName }}Interceptors = {{ $.InterPkg }}.New{{ .StructName }}ServerInterceptors()
+		{{- end }}
+	{{- end }}
+	}
+	{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/codegen/generator/example.go b/codegen/generator/example.go
index c78545a3..82788014 100644
--- a/codegen/generator/example.go
+++ b/codegen/generator/example.go
@@ -25,6 +25,11 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
 			files = append(files, fs...)
 		}
 
+		// example interceptors implementation
+		if fs := service.ExampleInterceptorsFiles(genpkg, r); len(fs) != 0 {
+			files = append(files, fs...)
+		}
+
 		// server main
 		if fs := example.ServerFiles(genpkg, r); len(fs) != 0 {
 			files = append(files, fs...)
@@ -54,6 +59,8 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
 				files = append(files, fs...)
 			}
 		}
+
+		// Add imports defined via struct:field:type
 		for _, f := range files {
 			if len(f.SectionTemplates) > 0 {
 				for _, s := range r.Services {
diff --git a/codegen/service/client.go b/codegen/service/client.go
index a088de0d..8cc61b2d 100644
--- a/codegen/service/client.go
+++ b/codegen/service/client.go
@@ -15,7 +15,7 @@ const (
 // ClientFile returns the client file for the given service.
 func ClientFile(_ string, service *expr.ServiceExpr) *codegen.File {
 	svc := Services.Get(service.Name)
-	data := endpointData(service)
+	data := endpointData(svc)
 	path := filepath.Join(codegen.Gendir, svc.PathName, "client.go")
 	var (
 		sections []*codegen.SectionTemplate
diff --git a/codegen/service/endpoint.go b/codegen/service/endpoint.go
index aaefc4bb..8ca71567 100644
--- a/codegen/service/endpoint.go
+++ b/codegen/service/endpoint.go
@@ -30,6 +30,12 @@ type (
 		// Schemes contains the security schemes types used by the
 		// all the endpoints.
 		Schemes SchemesData
+		// HasServerInterceptors indicates that the service has server-side
+		// interceptors.
+		HasServerInterceptors bool
+		// HasClientInterceptors indicates that the service has client-side
+		// interceptors.
+		HasClientInterceptors bool
 	}
 
 	// EndpointMethodData describes a single endpoint method.
@@ -61,7 +67,7 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File {
 	svc := Services.Get(service.Name)
 	svcName := svc.PathName
 	path := filepath.Join(codegen.Gendir, svcName, "endpoints.go")
-	data := endpointData(service)
+	data := endpointData(svc)
 	var (
 		sections []*codegen.SectionTemplate
 	)
@@ -128,8 +134,7 @@ func EndpointFile(genpkg string, service *expr.ServiceExpr) *codegen.File {
 	return &codegen.File{Path: path, SectionTemplates: sections}
 }
 
-func endpointData(service *expr.ServiceExpr) *EndpointsData {
-	svc := Services.Get(service.Name)
+func endpointData(svc *Data) *EndpointsData {
 	methods := make([]*EndpointMethodData, len(svc.Methods))
 	names := make([]string, len(svc.Methods))
 	for i, m := range svc.Methods {
@@ -142,16 +147,18 @@ func endpointData(service *expr.ServiceExpr) *EndpointsData {
 		}
 		names[i] = codegen.Goify(m.VarName, false)
 	}
-	desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, service.Name)
+	desc := fmt.Sprintf("%s wraps the %q service endpoints.", endpointsStructName, svc.Name)
 	return &EndpointsData{
-		Name:           service.Name,
-		Description:    desc,
-		VarName:        endpointsStructName,
-		ClientVarName:  clientStructName,
-		ServiceVarName: serviceInterfaceName,
-		ClientInitArgs: strings.Join(names, ", "),
-		Methods:        methods,
-		Schemes:        svc.Schemes,
+		Name:                  svc.Name,
+		Description:           desc,
+		VarName:               endpointsStructName,
+		ClientVarName:         clientStructName,
+		ServiceVarName:        serviceInterfaceName,
+		ClientInitArgs:        strings.Join(names, ", "),
+		Methods:               methods,
+		Schemes:               svc.Schemes,
+		HasServerInterceptors: len(svc.ServerInterceptors) > 0,
+		HasClientInterceptors: len(svc.ClientInterceptors) > 0,
 	}
 }
 
diff --git a/codegen/service/example_interceptors.go b/codegen/service/example_interceptors.go
new file mode 100644
index 00000000..7c24b08b
--- /dev/null
+++ b/codegen/service/example_interceptors.go
@@ -0,0 +1,86 @@
+package service
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+)
+
+// ExampleInterceptorsFiles returns the files for the example server and client interceptors.
+func ExampleInterceptorsFiles(genpkg string, r *expr.RootExpr) []*codegen.File {
+	var fw []*codegen.File
+	for _, svc := range r.Services {
+		if f := exampleInterceptorsFile(genpkg, svc); f != nil {
+			fw = append(fw, f...)
+		}
+	}
+	return fw
+}
+
+// exampleInterceptorsFile returns the example interceptors for the given service.
+func exampleInterceptorsFile(genpkg string, svc *expr.ServiceExpr) []*codegen.File {
+	sdata := Services.Get(svc.Name)
+	data := map[string]any{
+		"ServiceName":        sdata.Name,
+		"StructName":         sdata.StructName,
+		"PkgName":            "interceptors",
+		"ServerInterceptors": sdata.ServerInterceptors,
+		"ClientInterceptors": sdata.ClientInterceptors,
+	}
+
+	var files []*codegen.File
+
+	// Generate server interceptor if needed and file doesn't exist
+	if len(sdata.ServerInterceptors) > 0 {
+		serverPath := filepath.Join("interceptors", sdata.PathName+"_server.go")
+		if _, err := os.Stat(serverPath); os.IsNotExist(err) {
+			files = append(files, &codegen.File{
+				Path: serverPath,
+				SectionTemplates: []*codegen.SectionTemplate{
+					codegen.Header(fmt.Sprintf("%s example server interceptors", sdata.Name), "interceptors", []*codegen.ImportSpec{
+						{Path: "context"},
+						{Path: "fmt"},
+						{Path: "goa.design/clue/log"},
+						codegen.GoaImport(""),
+						{Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName},
+					}),
+					{
+						Name:   "exmaple-server-interceptor",
+						Source: readTemplate("example_server_interceptor"),
+						Data:   data,
+					},
+				},
+			})
+		}
+	}
+
+	// Generate client interceptor if needed and file doesn't exist
+	if len(sdata.ClientInterceptors) > 0 {
+		clientPath := filepath.Join("interceptors", sdata.PathName+"_client.go")
+		if _, err := os.Stat(clientPath); os.IsNotExist(err) {
+			files = append(files, &codegen.File{
+				Path: clientPath,
+				SectionTemplates: []*codegen.SectionTemplate{
+					codegen.Header(fmt.Sprintf("%s example client interceptors", sdata.Name), "interceptors", []*codegen.ImportSpec{
+						{Path: "context"},
+						{Path: "fmt"},
+						{Path: "goa.design/clue/log"},
+						codegen.GoaImport(""),
+						{Path: path.Join(genpkg, sdata.PathName), Name: sdata.PkgName},
+					}),
+					{
+						Name:   "example-client-interceptor",
+						Source: readTemplate("example_client_interceptor"),
+						Data:   data,
+					},
+				},
+			})
+		}
+	}
+
+	return files
+}
diff --git a/codegen/service/example_svc.go b/codegen/service/example_svc.go
index 7f4b1560..46b977ef 100644
--- a/codegen/service/example_svc.go
+++ b/codegen/service/example_svc.go
@@ -78,18 +78,18 @@ func exampleServiceFile(genpkg string, _ *expr.RootExpr, svc *expr.ServiceExpr,
 		codegen.Header("", apipkg, specs),
 		{
 			Name:   "basic-service-struct",
-			Source: readTemplate("service_struct"),
+			Source: readTemplate("example_service_struct"),
 			Data:   data,
 		}, {
 			Name:   "basic-service-init",
-			Source: readTemplate("service_init"),
+			Source: readTemplate("example_service_init"),
 			Data:   data,
 		},
 	}
 	if len(data.Schemes) > 0 {
 		sections = append(sections, &codegen.SectionTemplate{
 			Name:   "security-authfuncs",
-			Source: readTemplate("security_authfuncs"),
+			Source: readTemplate("example_security_authfuncs"),
 			Data:   data,
 		})
 	}
diff --git a/codegen/service/interceptors.go b/codegen/service/interceptors.go
new file mode 100644
index 00000000..8da90f9f
--- /dev/null
+++ b/codegen/service/interceptors.go
@@ -0,0 +1,179 @@
+package service
+
+import (
+	"path/filepath"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+)
+
+// InterceptorsFiles returns the interceptors files for the given service.
+func InterceptorsFiles(genpkg string, service *expr.ServiceExpr) []*codegen.File {
+	var files []*codegen.File
+	svc := Services.Get(service.Name)
+
+	// Generate service-specific interceptor files
+	if len(svc.ServerInterceptors) > 0 {
+		files = append(files, interceptorFile(svc, true))
+	}
+	if len(svc.ClientInterceptors) > 0 {
+		files = append(files, interceptorFile(svc, false))
+	}
+
+	// Generate wrapper file if this service has any interceptors
+	if len(svc.ServerInterceptors) > 0 || len(svc.ClientInterceptors) > 0 {
+		files = append(files, wrapperFile(svc))
+	}
+
+	return files
+}
+
+// interceptorFile returns the file defining the interceptors.
+// This method is called twice, once for the server and once for the client.
+func interceptorFile(svc *Data, server bool) *codegen.File {
+	filename := "client_interceptors.go"
+	template := "client_interceptors"
+	section := "client-interceptors-type"
+	desc := "Client Interceptors"
+	if server {
+		filename = "service_interceptors.go"
+		template = "server_interceptors"
+		section = "server-interceptors-type"
+		desc = "Server Interceptors"
+	}
+	desc = svc.Name + desc
+	path := filepath.Join(codegen.Gendir, svc.PathName, filename)
+
+	interceptors := svc.ServerInterceptors
+	if !server {
+		interceptors = svc.ClientInterceptors
+	}
+
+	// We don't want to generate duplicate interceptor info data structures for
+	// interceptors that are both server and client side so remove interceptors
+	// that are both server and client side when generating the client.
+	if !server {
+		names := make(map[string]struct{}, len(svc.ServerInterceptors))
+		for _, sin := range svc.ServerInterceptors {
+			names[sin.Name] = struct{}{}
+		}
+		filtered := make([]*InterceptorData, 0, len(interceptors))
+		for _, in := range interceptors {
+			if _, ok := names[in.Name]; !ok {
+				filtered = append(filtered, in)
+			}
+		}
+		interceptors = filtered
+	}
+
+	sections := []*codegen.SectionTemplate{
+		codegen.Header(desc, svc.PkgName, []*codegen.ImportSpec{
+			{Path: "context"},
+			codegen.GoaImport(""),
+		}),
+		{
+			Name:   section,
+			Source: readTemplate(template),
+			Data:   svc,
+		},
+	}
+	if len(interceptors) > 0 {
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   "interceptor-types",
+			Source: readTemplate("interceptors_types"),
+			Data:   interceptors,
+			FuncMap: map[string]any{
+				"hasPrivateImplementationTypes": hasPrivateImplementationTypes,
+			},
+		})
+	}
+
+	template = "endpoint_wrappers"
+	section = "endpoint-wrapper"
+	if !server {
+		template = "client_wrappers"
+		section = "client-wrapper"
+	}
+	for _, m := range svc.Methods {
+		ints := m.ServerInterceptors
+		if !server {
+			ints = m.ClientInterceptors
+		}
+		if len(ints) == 0 {
+			continue
+		}
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   section,
+			Source: readTemplate(template),
+			Data: map[string]interface{}{
+				"MethodVarName": m.VarName,
+				"Method":        m.Name,
+				"Service":       svc.Name,
+				"Interceptors":  ints,
+			},
+		})
+	}
+
+	if len(interceptors) > 0 {
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   "interceptors",
+			Source: readTemplate("interceptors"),
+			Data:   interceptors,
+			FuncMap: map[string]any{
+				"hasPrivateImplementationTypes": hasPrivateImplementationTypes,
+			},
+		})
+	}
+
+	return &codegen.File{Path: path, SectionTemplates: sections}
+}
+
+// wrapperFile returns the file containing the interceptor wrappers.
+func wrapperFile(svc *Data) *codegen.File {
+	path := filepath.Join(codegen.Gendir, svc.PathName, "interceptor_wrappers.go")
+
+	var sections []*codegen.SectionTemplate
+	sections = append(sections, codegen.Header("Interceptor wrappers", svc.PkgName, []*codegen.ImportSpec{
+		{Path: "context"},
+		{Path: "fmt"},
+		codegen.GoaImport(""),
+	}))
+
+	// Generate the interceptor wrapper functions first (only once)
+	if len(svc.ServerInterceptors) > 0 {
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   "server-interceptor-wrappers",
+			Source: readTemplate("server_interceptor_wrappers"),
+			Data: map[string]interface{}{
+				"Service":            svc.Name,
+				"ServerInterceptors": svc.ServerInterceptors,
+			},
+		})
+	}
+	if len(svc.ClientInterceptors) > 0 {
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   "client-interceptor-wrappers",
+			Source: readTemplate("client_interceptor_wrappers"),
+			Data: map[string]interface{}{
+				"Service":            svc.Name,
+				"ClientInterceptors": svc.ClientInterceptors,
+			},
+		})
+	}
+
+	return &codegen.File{
+		Path:             path,
+		SectionTemplates: sections,
+	}
+}
+
+// hasPrivateImplementationTypes returns true if any of the interceptors have
+// private implementation types.
+func hasPrivateImplementationTypes(interceptors []*InterceptorData) bool {
+	for _, intr := range interceptors {
+		if intr.ReadPayload != nil || intr.WritePayload != nil || intr.ReadResult != nil || intr.WriteResult != nil {
+			return true
+		}
+	}
+	return false
+}
diff --git a/codegen/service/interceptors.md b/codegen/service/interceptors.md
new file mode 100644
index 00000000..80795d3d
--- /dev/null
+++ b/codegen/service/interceptors.md
@@ -0,0 +1,149 @@
+# Interceptors Code Generation
+
+Goa generates interceptor code to enable request/response interception and payload/result access. 
+
+---
+
+## 1. Client & Server Interceptors
+
+### Where They Are Generated
+
+Client and server interceptor code is generated in:
+
+- `gen/client_interceptors.go`
+- `gen/service_interceptors.go`
+
+### Templates Used
+
+1. **`server_interceptors.go.tpl`** and **`client_interceptors.go.tpl`**  
+   Define the server and client interceptor interface types.
+   Each template takes a `Data` struct as input.
+
+2. **`interceptors.go.tpl`**  
+   Generates the payload/result access interfaces and accompanying info structs.
+   This template takes a slice of `InterceptorData` as input.
+
+3. **`client_wrappers.go.tpl`** and **`endpoint_wrappers.go.tpl`**  
+   Generate code that wraps client and service endpoints with interceptor callbacks.
+   Each template takes a map with:
+   ```go
+   map[string]any{
+   	"MethodVarName": <Implemented method name>
+		"Method":        <Design method name>
+		"Service":       <Service name>
+		"Interceptors":  <Slice of InterceptorData>
+   }
+   ```
+
+---
+
+## 2. Client & Server Endpoint Wrapper Code
+
+### Where It Is Generated
+
+Endpoint wrapper code for both client and server interceptors is generated in:
+
+- `gen/interceptor_wrappers.go`
+
+### Templates Used
+
+1. **`server_interceptor_wrappers.go.tpl`**  
+   Generates server-specific wrapper implementations. This template receives a map with:
+   ```go
+   map[string]any{
+     "Service":            svc.Name,
+     "ServerInterceptors": svc.ServerInterceptors,
+   }
+   ```
+
+2. **`client_interceptor_wrappers.go.tpl`**  
+   Generates client-specific wrapper implementations. This template receives a map with:
+   ```go
+   map[string]any{
+     "Service":            svc.Name,
+     "ClientInterceptors": svc.ClientInterceptors,
+   }
+   ```
+
+## 3. Example Interceptors
+
+### Where They Are Generated
+
+Example interceptors are generated by the example command in an interceptors sub-package of the user’s service package:
+
+* Client interceptors: `<service>_client.go`
+* Server interceptors: `<service>_server.go`
+
+### Templates Used
+
+1. **`example_client_interceptor.go.tpl` and `example_server_interceptor.go.tpl`**  
+   Generate example interceptor implementations. Each template takes a map with:
+   ```go
+   map[string]any{
+     "StructName":         <interceptor struct name>
+     "ServiceName":        <service name>
+     "PkgName":            <interceptors package name>
+     "ServerInterceptors": <server interceptors slice>
+     "ClientInterceptors": <client interceptors slice>
+   }
+   ```
+
+2. **`example_service_init.go.tpl`**  
+   Generates an example service implementation. This template takes a Data struct.
+
+### Generated Example Features
+
+The example interceptors demonstrate common interception patterns:
+
+* Logging of request/response data
+* Error handling and error logging
+* Type-safe payload and result access through the generated info structs
+* Context propagation across requests and responses
+
+## 4. Data Structures
+
+The templates above share a common set of data structures that describe
+interceptor behavior, payload, result access, and streaming. These data
+structures are defined as follows:
+
+### `Data`
+
+A service-level structure containing information for both client and server interceptors:
+
+* `ServerInterceptors`: A slice of InterceptorData for server-side interceptors
+* `ClientInterceptors`: A slice of InterceptorData for client-side interceptors
+
+### `InterceptorData`
+
+The main structure describing each interceptor’s metadata and requirements:
+
+* `Name`: Generated Go name of the interceptor (CamelCase)
+* `DesignName`: Original name from the design
+* `Description`: Interceptor description from the design
+* `HasPayloadAccess`: Indicates if any method requires payload access
+* `HasResultAccess`: Indicates if any method requires result access
+* `ReadPayload`: List of readable payload fields ([]AttributeData)
+* `WritePayload`: List of writable payload fields ([]AttributeData)
+* `ReadResult`: List of readable result fields ([]AttributeData)
+* `WriteResult`: List of writable result fields ([]AttributeData)
+* `Methods`: A list of MethodInterceptorData containing method-specific interceptor information
+* `ServerStreamInputStruct`: Server stream variable name (used if streaming)
+* `ClientStreamInputStruct`: Client stream variable name (used if streaming)
+
+### `MethodInterceptorData`
+
+Stores per-method interceptor configuration:
+
+* `MethodName`: The method’s Go variable name
+* `PayloadAccess`: Name of the payload access type
+* `ResultAccess`: Name of the result access type
+* `PayloadRef`: Reference to the method's payload type
+* `ResultRef`: Reference to the method's result type
+
+### `AttributeData`
+
+Represents per-field access configuration:
+
+* `Name`: The field accessor method name
+* `TypeRef`: Go type reference for the field
+* `Type`: Underlying attribute type information
diff --git a/codegen/service/service.go b/codegen/service/service.go
index 8d920c78..706ea255 100644
--- a/codegen/service/service.go
+++ b/codegen/service/service.go
@@ -195,6 +195,9 @@ func Files(genpkg string, service *expr.ServiceExpr, userTypePkgs map[string][]s
 	}
 	files := []*codegen.File{{Path: svcPath, SectionTemplates: sections}}
 
+	// service and client interceptors
+	files = append(files, InterceptorsFiles(genpkg, service)...)
+
 	// user types
 	paths := make([]string, len(typeDefSections))
 	i := 0
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index f0a62f81..7270e1e4 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -37,12 +37,6 @@ type (
 	// ServicesData encapsulates the data computed from the service designs.
 	ServicesData map[string]*Data
 
-	// RequirementsData is the list of security requirements.
-	RequirementsData []*RequirementData
-
-	// SchemesData is the list of security schemes.
-	SchemesData []*SchemeData
-
 	// Data contains the data used to render the code related to a single
 	// service.
 	Data struct {
@@ -70,6 +64,12 @@ type (
 		Methods []*MethodData
 		// Schemes is the list of security schemes required by the service methods.
 		Schemes SchemesData
+		// ServerInterceptors contains the data needed to render the server-side
+		// interceptors code.
+		ServerInterceptors []*InterceptorData
+		// ClientInterceptors contains the data needed to render the client-side
+		// interceptors code.
+		ClientInterceptors []*InterceptorData
 		// Scope initialized with all the service types.
 		Scope *codegen.NameScope
 		// ViewScope initialized with all the viewed types.
@@ -99,38 +99,6 @@ type (
 		unionValueMethods []*UnionValueMethodData
 	}
 
-	// UnionValueMethodData describes a method used on a union value type.
-	UnionValueMethodData struct {
-		// Name is the name of the function.
-		Name string
-		// TypeRef is a reference on the target union value type.
-		TypeRef string
-		// Loc defines the file and Go package of the method if
-		// overridden in corresponding union type via Meta.
-		Loc *codegen.Location
-	}
-
-	// ErrorInitData describes an error returned by a service method of type
-	// ErrorResult.
-	ErrorInitData struct {
-		// Name is the name of the init function.
-		Name string
-		// Description is the error description.
-		Description string
-		// ErrName is the name of the error.
-		ErrName string
-		// TypeName is the error struct type name.
-		TypeName string
-		// TypeRef is the reference to the error type.
-		TypeRef string
-		// Temporary indicates whether the error is temporary.
-		Temporary bool
-		// Timeout indicates whether the error is due to timeouts.
-		Timeout bool
-		// Fault indicates whether the error is server-side fault.
-		Fault bool
-	}
-
 	// MethodData describes a single service method.
 	MethodData struct {
 		// Name is the method name.
@@ -188,6 +156,12 @@ type (
 		// Schemes contains the security schemes types used by the
 		// method.
 		Schemes SchemesData
+		// ServerInterceptors list the server interceptors that apply to this
+		// method.
+		ServerInterceptors []string
+		// ClientInterceptors list the client interceptors that apply to this
+		// method.
+		ClientInterceptors []string
 		// ViewedResult contains the data required to generate the code handling
 		// views if any.
 		ViewedResult *ViewedResultTypeData
@@ -252,6 +226,94 @@ type (
 		Kind expr.StreamKind
 	}
 
+	// ErrorInitData describes an error returned by a service method of type
+	// ErrorResult.
+	ErrorInitData struct {
+		// Name is the name of the init function.
+		Name string
+		// Description is the error description.
+		Description string
+		// ErrName is the name of the error.
+		ErrName string
+		// TypeName is the error struct type name.
+		TypeName string
+		// TypeRef is the reference to the error type.
+		TypeRef string
+		// Temporary indicates whether the error is temporary.
+		Temporary bool
+		// Timeout indicates whether the error is due to timeouts.
+		Timeout bool
+		// Fault indicates whether the error is server-side fault.
+		Fault bool
+	}
+
+	// InterceptorData contains the data required to render the service-level
+	// interceptor code. interceptors.go.tpl
+	InterceptorData struct {
+		// Name is the name of the interceptor used in the generated code.
+		Name string
+		// DesignName is the name of the interceptor as defined in the design.
+		DesignName string
+		// Description is the description of the interceptor from the design.
+		Description string
+		// Methods
+		Methods []*MethodInterceptorData
+		// ReadPayload contains payload attributes that the interceptor can
+		// read.
+		ReadPayload []*AttributeData
+		// WritePayload contains payload attributes that the interceptor can
+		// write.
+		WritePayload []*AttributeData
+		// ReadResult contains result attributes that the interceptor can read.
+		ReadResult []*AttributeData
+		// WriteResult contains result attributes that the interceptor can
+		// write.
+		WriteResult []*AttributeData
+		// HasPayloadAccess indicates that the interceptor info object has a
+		// payload access interface.
+		HasPayloadAccess bool
+		// HasResultAccess indicates that the interceptor info object has a
+		// result access interface.
+		HasResultAccess bool
+	}
+
+	// MethodInterceptorData contains the data required to render the
+	// method-level interceptor code.
+	MethodInterceptorData struct {
+		// MethodName is the name of the method.
+		MethodName string
+		// PayloadAccess is the name of the payload access struct.
+		PayloadAccess string
+		// ResultAccess is the name of the result access struct.
+		ResultAccess string
+		// PayloadRef is the reference to the method payload type.
+		PayloadRef string
+		// ResultRef is the reference to the method result type.
+		ResultRef string
+		// ServerStreamInputStruct is the name of the server stream input
+		// struct if the endpoint defines a server stream.
+		ServerStreamInputStruct string
+		// ClientStreamInputStruct is the name of the client stream input
+		// struct if the endpoint defines a client stream.
+		ClientStreamInputStruct string
+	}
+
+	// AttributeData describes a single attribute.
+	AttributeData struct {
+		// Name is the name of the attribute.
+		Name string
+		// TypeRef is the reference to the attribute type.
+		TypeRef string
+		// Pointer is true if the attribute is a pointer.
+		Pointer bool
+	}
+
+	// RequirementsData is the list of security requirements.
+	RequirementsData []*RequirementData
+
+	// SchemesData is the list of security schemes.
+	SchemesData []*SchemeData
+
 	// RequirementData lists the schemes and scopes defined by a single
 	// security requirement.
 	RequirementData struct {
@@ -280,6 +342,17 @@ type (
 		Type expr.UserType
 	}
 
+	// UnionValueMethodData describes a method used on a union value type.
+	UnionValueMethodData struct {
+		// Name is the name of the function.
+		Name string
+		// TypeRef is a reference on the target union value type.
+		TypeRef string
+		// Loc defines the file and Go package of the method if
+		// overridden in corresponding union type via Meta.
+		Loc *codegen.Location
+	}
+
 	// SchemeData describes a single security scheme.
 	SchemeData struct {
 		// Kind is the type of scheme, one of "Basic", "APIKey", "JWT"
@@ -769,6 +842,8 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 		ViewsPkg:           viewspkg,
 		Methods:            methods,
 		Schemes:            schemes,
+		ServerInterceptors: collectInterceptors(service, methods, scope, true),
+		ClientInterceptors: collectInterceptors(service, methods, scope, false),
 		Scope:              scope,
 		ViewScope:          viewScope,
 		errorTypes:         errTypes,
@@ -779,11 +854,47 @@ func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
 		viewedResultTypes:  viewedRTs,
 		unionValueMethods:  unionMethods,
 	}
+
 	d[service.Name] = data
 
 	return data
 }
 
+// collectInterceptors returns the set of interceptors defined on the given
+// service including any interceptor defined on specific service methods or API.
+func collectInterceptors(svc *expr.ServiceExpr, methods []*MethodData, scope *codegen.NameScope, server bool) []*InterceptorData {
+	var ints []*expr.InterceptorExpr
+	if server {
+		ints = expr.Root.API.ServerInterceptors
+		ints = append(ints, svc.ServerInterceptors...)
+		for _, m := range svc.Methods {
+			ints = append(ints, m.ServerInterceptors...)
+		}
+	} else {
+		ints = expr.Root.API.ClientInterceptors
+		ints = append(ints, svc.ClientInterceptors...)
+		for _, m := range svc.Methods {
+			ints = append(ints, m.ClientInterceptors...)
+		}
+	}
+	// remove duplicate interceptors
+	sort.Slice(ints, func(i, j int) bool {
+		return ints[i].Name < ints[j].Name
+	})
+	for i := 1; i < len(ints); i++ {
+		if ints[i-1].Name == ints[i].Name {
+			ints = append(ints[:i], ints[i+1:]...)
+			i--
+		}
+	}
+
+	res := make([]*InterceptorData, 0, len(ints))
+	for _, i := range ints {
+		res = append(res, buildInterceptorData(svc, methods, i, scope, server))
+	}
+	return res
+}
+
 // typeContext returns a contextual attribute for service types. Service types
 // are Go types and uses non-pointers to hold attributes having default values.
 func typeContext(pkg string, scope *codegen.NameScope) *codegen.AttributeContext {
@@ -1001,14 +1112,15 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
 		RequestStruct:                vname + "RequestData",
 		ResponseStruct:               vname + "ResponseData",
 	}
-	if m.IsStreaming() {
-		initStreamData(data, m, vname, rname, resultRef, scope)
-	}
+	initStreamData(data, m, vname, rname, resultRef, scope)
 	return data
 }
 
 // initStreamData initializes the streaming payload data structures and methods.
 func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRef string, scope *codegen.NameScope) {
+	if !m.IsStreaming() {
+		return
+	}
 	var (
 		spayloadName string
 		spayloadRef  string
@@ -1082,6 +1194,87 @@ func initStreamData(data *MethodData, m *expr.MethodExpr, vname, rname, resultRe
 	data.StreamingPayloadEx = spayloadEx
 }
 
+// buildInterceptorData creates the data needed to generate interceptor code.
+func buildInterceptorData(svc *expr.ServiceExpr, methods []*MethodData, i *expr.InterceptorExpr, scope *codegen.NameScope, server bool) *InterceptorData {
+	data := &InterceptorData{
+		Name:        codegen.Goify(i.Name, true),
+		DesignName:  i.Name,
+		Description: i.Description,
+	}
+	if len(svc.Methods) == 0 {
+		return data
+	}
+	payload, result := svc.Methods[0].Payload, svc.Methods[0].Result
+	data.ReadPayload = collectAttributes(i.ReadPayload, payload, scope)
+	data.WritePayload = collectAttributes(i.WritePayload, payload, scope)
+	data.ReadResult = collectAttributes(i.ReadResult, result, scope)
+	data.WriteResult = collectAttributes(i.WriteResult, result, scope)
+	if len(data.ReadPayload) > 0 || len(data.WritePayload) > 0 {
+		data.HasPayloadAccess = true
+	}
+	if len(data.ReadResult) > 0 || len(data.WriteResult) > 0 {
+		data.HasResultAccess = true
+	}
+	for _, m := range svc.Methods {
+		applies := false
+		intExprs := m.ServerInterceptors
+		if !server {
+			intExprs = m.ClientInterceptors
+		}
+		for _, in := range intExprs {
+			if in.Name == i.Name {
+				applies = true
+				break
+			}
+		}
+		if !applies {
+			continue
+		}
+		var md *MethodData
+		for _, mt := range methods {
+			if m.Name == mt.Name {
+				md = mt
+				break
+			}
+		}
+		data.Methods = append(data.Methods, buildInterceptorMethodData(i, md))
+		if server {
+			md.ServerInterceptors = append(md.ServerInterceptors, i.Name)
+		} else {
+			md.ClientInterceptors = append(md.ClientInterceptors, i.Name)
+		}
+	}
+	return data
+}
+
+// buildIntercetorMethodData creates the data needed to generate interceptor
+// method code.
+func buildInterceptorMethodData(i *expr.InterceptorExpr, md *MethodData) *MethodInterceptorData {
+	var serverStream, clientStream string
+	if md.ServerStream != nil {
+		serverStream = md.ServerStream.VarName
+	}
+	if md.ClientStream != nil {
+		clientStream = md.ClientStream.VarName
+	}
+	var payloadAccess, resultAccess string
+	if i.ReadPayload != nil || i.WritePayload != nil {
+		payloadAccess = codegen.Goify(i.Name, false) + md.VarName + "Payload"
+	}
+	if i.ReadResult != nil || i.WriteResult != nil {
+		resultAccess = codegen.Goify(i.Name, false) + md.VarName + "Result"
+	}
+	return &MethodInterceptorData{
+		MethodName:              md.VarName,
+		PayloadAccess:           payloadAccess,
+		ResultAccess:            resultAccess,
+		PayloadRef:              md.PayloadRef,
+		ResultRef:               md.ResultRef,
+		ClientStreamInputStruct: clientStream,
+		ServerStreamInputStruct: serverStream,
+	}
+}
+
 // BuildSchemeData builds the scheme data for the given scheme and method expr.
 func BuildSchemeData(s *expr.SchemeExpr, m *expr.MethodExpr) *SchemeData {
 	if !expr.IsObject(m.Payload.Type) {
@@ -1184,6 +1377,30 @@ func BuildSchemeData(s *expr.SchemeExpr, m *expr.MethodExpr) *SchemeData {
 	return nil
 }
 
+// collectAttributes builds AttributeData from an AttributeExpr
+func collectAttributes(attrNames, parent *expr.AttributeExpr, scope *codegen.NameScope) []*AttributeData {
+	if attrNames == nil {
+		return nil
+	}
+	obj := expr.AsObject(attrNames.Type)
+	if obj == nil {
+		return nil
+	}
+	data := make([]*AttributeData, len(*obj))
+	for i, nat := range *obj {
+		parentAttr := parent.Find(nat.Name)
+		if parentAttr == nil {
+			continue
+		}
+		data[i] = &AttributeData{
+			Name:    codegen.Goify(nat.Name, true),
+			TypeRef: scope.GoTypeRef(parentAttr),
+			Pointer: parent.IsPrimitivePointer(nat.Name, true),
+		}
+	}
+	return data
+}
+
 // collectProjectedTypes builds a projected type for every user type found
 // when recursing through the attributes. The projected types live in the views
 // package and support the marshaling and unmarshalling of result types that
diff --git a/codegen/service/templates/client_interceptor_wrappers.go.tpl b/codegen/service/templates/client_interceptor_wrappers.go.tpl
new file mode 100644
index 00000000..cc0ff52a
--- /dev/null
+++ b/codegen/service/templates/client_interceptor_wrappers.go.tpl
@@ -0,0 +1,22 @@
+{{- range .ClientInterceptors }}
+{{-  $interceptor := . }}
+{{- range .Methods }}
+
+{{ comment (printf "wrapClient%s%s applies the %s client interceptor to endpoints." $interceptor.Name .MethodName $interceptor.DesignName) }}
+func wrapClient{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+    return func(ctx context.Context, req any) (any, error) {
+        info := &{{ $interceptor.Name }}Info{
+            Service:    "{{ $.Service }}",
+            Method:     "{{ .MethodName }}",
+            Endpoint:   endpoint,
+            {{- if .ClientStreamInputStruct }}
+            RawPayload: req.(*{{ .ClientStreamInputStruct }}).Payload,
+            {{- else }}
+            RawPayload: req,
+            {{- end }}
+        }
+        return i.{{ $interceptor.Name }}(ctx, info, endpoint)
+    }
+}
+{{ end }}
+{{- end }}
diff --git a/codegen/service/templates/client_interceptors.go.tpl b/codegen/service/templates/client_interceptors.go.tpl
new file mode 100644
index 00000000..9528e3d1
--- /dev/null
+++ b/codegen/service/templates/client_interceptors.go.tpl
@@ -0,0 +1,12 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+{{- range .ClientInterceptors }}
+{{- if .Description }}
+	{{ comment .Description }}
+{{- end }}
+	{{ .Name }}(ctx context.Context, info *{{ .Name }}Info, next goa.Endpoint) (any, error)
+{{- end }}
+}
diff --git a/codegen/service/templates/client_wrappers.go.tpl b/codegen/service/templates/client_wrappers.go.tpl
new file mode 100644
index 00000000..9287d25b
--- /dev/null
+++ b/codegen/service/templates/client_wrappers.go.tpl
@@ -0,0 +1,8 @@
+
+{{ comment (printf "Wrap%sClientEndpoint wraps the %s endpoint with the client interceptors defined in the design." .MethodVarName .Method) }}
+func Wrap{{ .MethodVarName }}ClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+    {{- range .Interceptors }}
+    endpoint = wrapClient{{ $.MethodVarName }}{{ . }}(endpoint, i)
+    {{- end }}
+    return endpoint
+}
diff --git a/codegen/service/templates/endpoint_wrappers.go.tpl b/codegen/service/templates/endpoint_wrappers.go.tpl
new file mode 100644
index 00000000..deffd14f
--- /dev/null
+++ b/codegen/service/templates/endpoint_wrappers.go.tpl
@@ -0,0 +1,7 @@
+{{ comment (printf "Wrap%sEndpoint wraps the %s endpoint with the server-side interceptors defined in the design." .MethodVarName .Method) }}
+func Wrap{{ .MethodVarName }}Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	{{- range .Interceptors }}
+	endpoint = wrap{{ $.MethodVarName }}{{ . }}(endpoint, i)
+	{{- end }}
+	return endpoint
+}
diff --git a/codegen/service/templates/example_client_interceptor.go.tpl b/codegen/service/templates/example_client_interceptor.go.tpl
new file mode 100644
index 00000000..455c74c9
--- /dev/null
+++ b/codegen/service/templates/example_client_interceptor.go.tpl
@@ -0,0 +1,24 @@
+// {{ .StructName }}ClientInterceptors implements the client interceptors for the {{ .ServiceName }} service.
+type {{ .StructName }}ClientInterceptors struct {
+}
+
+// New{{ .StructName }}ClientInterceptors creates a new client interceptor for the {{ .ServiceName }} service.
+func New{{ .StructName }}ClientInterceptors() *{{ .StructName }}ClientInterceptors {
+	return &{{ .StructName }}ClientInterceptors{}
+}
+
+{{- range .ClientInterceptors }}
+{{- if .Description }}
+{{ comment .Description }}
+{{- end }}
+func (i *{{ $.StructName }}ClientInterceptors) {{ .Name }}(ctx context.Context, info *{{ $.PkgName }}.{{ .Name }}Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[{{ .Name }}] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[{{ .Name }}] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[{{ .Name }}] Received response: %v", resp)
+	return resp, nil
+}
+{{- end }}
\ No newline at end of file
diff --git a/codegen/service/templates/security_authfuncs.go.tpl b/codegen/service/templates/example_security_authfuncs.go.tpl
similarity index 100%
rename from codegen/service/templates/security_authfuncs.go.tpl
rename to codegen/service/templates/example_security_authfuncs.go.tpl
diff --git a/codegen/service/templates/example_server_interceptor.go.tpl b/codegen/service/templates/example_server_interceptor.go.tpl
new file mode 100644
index 00000000..dfbb7d93
--- /dev/null
+++ b/codegen/service/templates/example_server_interceptor.go.tpl
@@ -0,0 +1,24 @@
+// {{ .StructName }}ServerInterceptors implements the server interceptor for the {{ .ServiceName }} service.
+type {{ .StructName }}ServerInterceptors struct {
+}
+
+// New{{ .StructName }}ServerInterceptors creates a new server interceptor for the {{ .ServiceName }} service.
+func New{{ .StructName }}ServerInterceptors() *{{ .StructName }}ServerInterceptors {
+	return &{{ .StructName }}ServerInterceptors{}
+}
+
+{{- range .ServerInterceptors }}
+{{- if .Description }}
+{{ comment .Description }}
+{{- end }}
+func (i *{{ $.StructName }}ServerInterceptors) {{ .Name }}(ctx context.Context, info *{{ $.PkgName }}.{{ .Name }}Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[{{ .Name }}] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[{{ .Name }}] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[{{ .Name }}] Response: %v", resp)
+	return resp, nil
+}
+{{- end }}
\ No newline at end of file
diff --git a/codegen/service/templates/service_init.go.tpl b/codegen/service/templates/example_service_init.go.tpl
similarity index 100%
rename from codegen/service/templates/service_init.go.tpl
rename to codegen/service/templates/example_service_init.go.tpl
diff --git a/codegen/service/templates/service_struct.go.tpl b/codegen/service/templates/example_service_struct.go.tpl
similarity index 100%
rename from codegen/service/templates/service_struct.go.tpl
rename to codegen/service/templates/example_service_struct.go.tpl
diff --git a/codegen/service/templates/interceptors.go.tpl b/codegen/service/templates/interceptors.go.tpl
new file mode 100644
index 00000000..ea3b0b5e
--- /dev/null
+++ b/codegen/service/templates/interceptors.go.tpl
@@ -0,0 +1,96 @@
+{{- if hasPrivateImplementationTypes . }}
+// Public accessor methods for Info types
+{{- range . }}
+	{{- if .HasPayloadAccess }}
+
+// Payload returns a type-safe accessor for the method payload.
+func (info *{{ .Name }}Info) Payload() {{ .Name }}Payload {
+		{{- if gt (len .Methods) 1 }}
+		switch info.Method {
+			{{- range .Methods }}
+		case "{{ .MethodName }}":
+			return &{{ .PayloadAccess }}{payload: info.RawPayload.({{ .PayloadRef }})}
+			{{- end }}
+		default:
+			return nil
+		}
+		{{- else }}
+	return &{{ (index .Methods 0).PayloadAccess }}{payload: info.RawPayload.({{ (index .Methods 0).PayloadRef }})}
+		{{- end }}
+}
+	{{- end }}
+
+	{{- if .HasResultAccess }}
+// Result returns a type-safe accessor for the method result.
+func (info *{{ .Name }}Info) Result(res any) {{ .Name }}Result {
+		{{- if gt (len .Methods) 1 }}
+	switch info.Method {
+			{{- range .Methods }}
+	case "{{ .MethodName }}":
+		return &{{ .ResultAccess }}{result: res.({{ .ResultRef }})}
+			{{- end }}
+	default:
+		return nil
+	}
+		{{- else }}
+	return &{{ (index .Methods 0).ResultAccess }}{result: res.({{ (index .Methods 0).ResultRef }})}
+		{{- end }}
+}
+	{{- end }}
+{{- end }}
+
+// Private implementation methods
+{{- range . }}
+	{{ $interceptor := . }}
+	{{- range .Methods }}
+		{{- $method := . }}
+		{{- range $interceptor.ReadPayload }}
+func (p *{{ $method.PayloadAccess }}) {{ .Name }}() {{ .TypeRef }} {
+			{{- if .Pointer }}
+	if p.payload.{{ .Name }} == nil {
+		var zero {{ .TypeRef }}
+		return zero
+	}
+	return *p.payload.{{ .Name }}
+			{{- else }}
+	return p.payload.{{ .Name }}
+			{{- end }}
+}
+		{{- end }}
+
+		{{- range $interceptor.WritePayload }}
+func (p *{{ $method.PayloadAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) {
+			{{- if .Pointer }}
+	p.payload.{{ .Name }} = &v
+			{{- else }}
+	p.payload.{{ .Name }} = v
+			{{- end }}
+}
+		{{- end }}
+
+		{{- range $interceptor.ReadResult }}
+func (r *{{ $method.ResultAccess }}) {{ .Name }}() {{ .TypeRef }} {
+			{{- if .Pointer }}
+	if r.result.{{ .Name }} == nil {
+		var zero {{ .TypeRef }}
+		return zero
+	}
+	return *r.result.{{ .Name }}
+			{{- else }}
+	return r.result.{{ .Name }}
+			{{- end }}
+}
+		{{- end }}
+
+		{{- range $interceptor.WriteResult }}
+func (r *{{ $method.ResultAccess }}) Set{{ .Name }}(v {{ .TypeRef }}) {
+			{{- if .Pointer }}
+	r.result.{{ .Name }} = &v
+			{{- else }}
+	r.result.{{ .Name }} = v
+			{{- end }}
+}
+		{{- end }}
+	{{- end }}
+{{- end }}
+{{- end }}
diff --git a/codegen/service/templates/interceptors_types.go.tpl b/codegen/service/templates/interceptors_types.go.tpl
new file mode 100644
index 00000000..0c542b08
--- /dev/null
+++ b/codegen/service/templates/interceptors_types.go.tpl
@@ -0,0 +1,62 @@
+
+// Access interfaces for interceptor payloads and results
+type (
+{{- range . }}
+	// {{ .Name }}Info provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	{{ .Name }}Info goa.InterceptorInfo
+	{{- if .HasPayloadAccess }}
+
+	// {{ .Name }}Payload provides type-safe access to the method payload.
+	// It allows reading and writing specific fields of the payload as defined
+	// in the design.
+	{{ .Name }}Payload interface {
+		{{- range .ReadPayload }}
+		{{ .Name }}() {{ .TypeRef }}
+		{{- end }}
+		{{- range .WritePayload }}
+		Set{{ .Name }}({{ .TypeRef }})
+		{{- end }}
+	}
+	{{- end }}
+	{{- if .HasResultAccess }}
+
+	// {{ .Name }}Result provides type-safe access to the method result.
+	// It allows reading and writing specific fields of the result as defined
+	// in the design.
+	{{ .Name }}Result interface {
+		{{- range .ReadResult }}
+		{{ .Name }}() {{ .TypeRef }}
+		{{- end }}
+		{{- range .WriteResult }}
+		Set{{ .Name }}({{ .TypeRef }})
+		{{- end }}
+	}
+	{{- end }}
+{{- end }}
+)
+{{- if hasPrivateImplementationTypes . }}
+
+// Private implementation types
+type (
+	{{- range . }}
+		{{- range .Methods }}
+			{{- if .PayloadAccess }}
+	{{ .PayloadAccess }} struct {
+		payload {{ .PayloadRef }}
+	}
+			{{- end }}
+		{{- end }}
+	{{- end }}
+
+	{{- range . }}
+		{{- range .Methods }}
+			{{- if .ResultAccess }}
+	{{ .ResultAccess }} struct {
+		result {{ .ResultRef }}
+	}
+			{{- end }}
+		{{- end }}
+	{{- end }}
+)
+{{- end }}
diff --git a/codegen/service/templates/server_interceptor_wrappers.go.tpl b/codegen/service/templates/server_interceptor_wrappers.go.tpl
new file mode 100644
index 00000000..cb0f5d9c
--- /dev/null
+++ b/codegen/service/templates/server_interceptor_wrappers.go.tpl
@@ -0,0 +1,22 @@
+{{- range .ServerInterceptors }}
+{{-  $interceptor := . }}
+{{- range .Methods }}
+
+{{ comment (printf "wrap%s%s applies the %s server interceptor to endpoints." $interceptor.Name .MethodName $interceptor.DesignName) }}
+func wrap{{ .MethodName }}{{ $interceptor.Name }}(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &{{ $interceptor.Name }}Info{
+			Service:    "{{ $.Service }}",
+			Method:     "{{ .MethodName }}",
+			Endpoint:   endpoint,
+			{{- if .ServerStreamInputStruct }}
+			RawPayload: req.(*{{ .ServerStreamInputStruct }}).Payload,
+			{{- else }}
+			RawPayload: req,
+			{{- end }}
+		}
+		return i.{{ $interceptor.Name }}(ctx, info, endpoint)
+	}
+}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/codegen/service/templates/server_interceptors.go.tpl b/codegen/service/templates/server_interceptors.go.tpl
new file mode 100644
index 00000000..cdfb9287
--- /dev/null
+++ b/codegen/service/templates/server_interceptors.go.tpl
@@ -0,0 +1,12 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+{{- range .ServerInterceptors }}
+	{{- if .Description }}
+	{{ comment .Description }}
+	{{- end }}
+	{{ .Name }}(ctx context.Context, info *{{ .Name }}Info, next goa.Endpoint) (any, error)
+{{- end }}
+}
\ No newline at end of file
diff --git a/codegen/service/templates/service_client_init.go.tpl b/codegen/service/templates/service_client_init.go.tpl
index 3f2302c5..20621066 100644
--- a/codegen/service/templates/service_client_init.go.tpl
+++ b/codegen/service/templates/service_client_init.go.tpl
@@ -1,8 +1,8 @@
 {{ printf "New%s initializes a %q service client given the endpoints." .ClientVarName .Name | comment }}
-func New{{ .ClientVarName }}({{ .ClientInitArgs }} goa.Endpoint) *{{ .ClientVarName }} {
-	return &{{ .ClientVarName }}{
-{{- range .Methods }}
-		{{ .VarName }}Endpoint: {{ .ArgName }},
-{{- end }}
-	}
+func New{{ .ClientVarName }}({{ .ClientInitArgs }} goa.Endpoint{{ if .HasClientInterceptors }}, ci ClientInterceptors{{ end }}) *{{ .ClientVarName }} {
+    return &{{ .ClientVarName }}{
+    {{- range .Methods }}
+        {{ .VarName }}Endpoint: {{ if .ClientInterceptors }}Wrap{{ .VarName }}ClientEndpoint({{ end }}{{ .ArgName }}{{ if .ClientInterceptors }}, ci){{ end }},
+    {{- end }}
+    }
 }
diff --git a/codegen/service/templates/service_endpoints_init.go.tpl b/codegen/service/templates/service_endpoints_init.go.tpl
index 64a9803a..3f46fc4d 100644
--- a/codegen/service/templates/service_endpoints_init.go.tpl
+++ b/codegen/service/templates/service_endpoints_init.go.tpl
@@ -1,14 +1,25 @@
 
-
 {{ printf "New%s wraps the methods of the %q service with endpoints." .VarName .Name | comment }}
-func New{{ .VarName }}(s {{ .ServiceVarName }}) *{{ .VarName }} {
+func New{{ .VarName }}(s {{ .ServiceVarName }}{{ if .HasServerInterceptors }}, si ServerInterceptors{{ end }}) *{{ .VarName }} {
 {{- if .Schemes }}
 	// Casting service to Auther interface
 	a := s.(Auther)
 {{- end }}
+{{- if .HasServerInterceptors }}
+	endpoints := &{{ .VarName }}{
+{{- else }}
 	return &{{ .VarName }}{
+{{- end }}
 {{- range .Methods }}
 		{{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes }}, a.{{ .Type }}Auth{{ end }}),
 {{- end }}
 	}
+{{- if .HasServerInterceptors }}
+	{{- range .Methods }}
+		{{- if .ServerInterceptors }}
+	endpoints.{{ .VarName }} = Wrap{{ .VarName }}Endpoint(endpoints.{{ .VarName }}, si)
+		{{- end }}
+	{{- end }}
+	return endpoints
+{{- end }}
 }
\ No newline at end of file
diff --git a/dsl/description.go b/dsl/description.go
index 931c4f2a..b8e9c784 100644
--- a/dsl/description.go
+++ b/dsl/description.go
@@ -45,6 +45,8 @@ func Description(d string) {
 		e.Description = d
 	case *expr.GRPCResponseExpr:
 		e.Description = d
+	case *expr.InterceptorExpr:
+		e.Description = d
 	default:
 		eval.IncompatibleDSL()
 	}
diff --git a/dsl/interceptor.go b/dsl/interceptor.go
new file mode 100644
index 00000000..fdb42fa1
--- /dev/null
+++ b/dsl/interceptor.go
@@ -0,0 +1,297 @@
+package dsl
+
+import (
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+// Interceptor defines a request interceptor. Interceptors provide a type-safe way
+// to read and write from and to the request and response.
+//
+// Interceptor must appear in a API, Service or Method expression.
+//
+// Interceptor accepts two arguments: the name of the interceptor and the
+// defining DSL.
+//
+// Example:
+//
+//	var Cache = Interceptor("Cache", func() {
+//	   Description("Server-side interceptor which implements a transparent cache for the loaded records")
+//
+//	   ReadPayload(func() {
+//	      Attribute("id")
+//	   })
+//
+//	   WriteResult(func() {
+//	      Attribute("cachedAt")
+//	   })
+//	})
+func Interceptor(name string, fn ...func()) *expr.InterceptorExpr {
+	if len(fn) > 1 {
+		eval.ReportError("interceptor %q cannot have multiple definitions", name)
+		return nil
+	}
+	i := &expr.InterceptorExpr{Name: name}
+	if name == "" {
+		eval.ReportError("interceptor name cannot be empty")
+		return i
+	}
+	if len(fn) > 0 {
+		if !eval.Execute(fn[0], i) {
+			return i
+		}
+	}
+	for _, i := range expr.Root.Interceptors {
+		if i.Name == name {
+			eval.ReportError("interceptor %q already defined", name)
+			return i
+		}
+	}
+	expr.Root.Interceptors = append(expr.Root.Interceptors, i)
+	return i
+}
+
+// ReadPayload defines the payload attributes read by the interceptor.
+//
+// ReadPayload must appear in an interceptor DSL.
+//
+// ReadPayload takes a function as argument which can use the Attribute DSL to
+// define the attributes read by the interceptor.
+//
+// Example:
+//
+//	ReadPayload(func() {
+//	   Attribute("id")
+//	})
+//
+// ReadPayload also accepts user defined types:
+//
+//	// Interceptor can read any payload field
+//	ReadPayload(MethodPayload)
+func ReadPayload(arg any) {
+	setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) {
+		i.ReadPayload = attr
+	})
+}
+
+// WritePayload defines the payload attributes written by the interceptor.
+//
+// WritePayload must appear in an interceptor DSL.
+//
+// WritePayload takes a function as argument which can use the Attribute DSL to
+// define the attributes written by the interceptor.
+//
+// Example:
+//
+//	WritePayload(func() {
+//	   Attribute("auth")
+//	})
+//
+// WritePayload also accepts user defined types:
+//
+//	// Interceptor can write any payload field
+//	WritePayload(MethodPayload)
+func WritePayload(arg any) {
+	setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) {
+		i.WritePayload = attr
+	})
+}
+
+// ReadResult defines the result attributes read by the interceptor.
+//
+// ReadResult must appear in an interceptor DSL.
+//
+// ReadResult takes a function as argument which can use the Attribute DSL to
+// define the attributes read by the interceptor.
+//
+// Example:
+//
+//	ReadResult(func() {
+//	   Attribute("cachedAt")
+//	})
+//
+// ReadResult also accepts user defined types:
+//
+//	// Interceptor can read any result field
+//	ReadResult(MethodResult)
+func ReadResult(arg any) {
+	setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) {
+		i.ReadResult = attr
+	})
+}
+
+// WriteResult defines the result attributes written by the interceptor.
+//
+// WriteResult must appear in an interceptor DSL.
+//
+// WriteResult takes a function as argument which can use the Attribute DSL to
+// define the attributes written by the interceptor.
+//
+// Example:
+//
+//	WriteResult(func() {
+//	   Attribute("cachedAt")
+//	})
+//
+// WriteResult also accepts user defined types:
+//
+//	// Interceptor can write any result field
+//	WriteResult(MethodResult)
+func WriteResult(arg any) {
+	setInterceptorAttribute(arg, func(i *expr.InterceptorExpr, attr *expr.AttributeExpr) {
+		i.WriteResult = attr
+	})
+}
+
+// ServerInterceptor lists the server-side interceptors that apply to all the
+// API endpoints, all the service endpoints or a specific endpoint.
+//
+// ServerInterceptor must appear in a API, Service or Method expression.
+//
+// ServerInterceptor accepts one or more interceptor or interceptor names as
+// arguments. ServerInterceptor can appear multiple times in the same DSL.
+//
+// Example:
+//
+//	Method("get_record", func() {
+//	   // Interceptor defined with the Interceptor DSL
+//	   ServerInterceptor(SetDeadline)
+//
+//	   // Name of interceptor defined with the Interceptor DSL
+//	   ServerInterceptor("Cache")
+//
+//	   // Interceptor defined inline
+//	   ServerInterceptor(Interceptor("CheckUserID", func() {
+//	      ReadPayload(func() {
+//	         Attribute("auth")
+//	      })
+//	   }))
+//
+//	   // ... rest of the method DSL
+//	})
+func ServerInterceptor(interceptors ...any) {
+	addInterceptors(interceptors, false)
+}
+
+// ClientInterceptor lists the client-side interceptors that apply to all the
+// API endpoints, all the service endpoints or a specific endpoint.
+//
+// ClientInterceptor must appear in a API, Service or Method expression.
+//
+// ClientInterceptor accepts one or more interceptor or interceptor names as
+// arguments. ClientInterceptor can appear multiple times in the same DSL.
+//
+// Example:
+//
+//	Method("get_record", func() {
+//	   // Interceptor defined with the Interceptor DSL
+//	   ClientInterceptor(Retry)
+//
+//	   // Name of interceptor defined with the Interceptor DSL
+//	   ClientInterceptor("Cache")
+//
+//	   // Interceptor defined inline
+//	   ClientInterceptor(Interceptor("Sign", func() {
+//	      ReadPayload(func() {
+//	         Attribute("user_id")
+//	      })
+//	      WritePayload(func() {
+//	         Attribute("auth")
+//	      })
+//	   }))
+//
+//	   // ... rest of the method DSL
+//	})
+func ClientInterceptor(interceptors ...any) {
+	addInterceptors(interceptors, true)
+}
+
+// setInterceptorAttribute is a helper function that handles the common logic for
+// setting interceptor attributes (ReadPayload, WritePayload, ReadResult, WriteResult).
+func setInterceptorAttribute(arg any, setter func(i *expr.InterceptorExpr, attr *expr.AttributeExpr)) {
+	i, ok := eval.Current().(*expr.InterceptorExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+
+	var attr *expr.AttributeExpr
+	switch fn := arg.(type) {
+	case func():
+		attr = &expr.AttributeExpr{Type: &expr.Object{}}
+		if !eval.Execute(fn, attr) {
+			return
+		}
+	case *expr.AttributeExpr:
+		attr = fn
+	case expr.DataType:
+		attr = &expr.AttributeExpr{Type: fn}
+	default:
+		eval.InvalidArgError("type, attribute or func()", arg)
+		return
+	}
+	setter(i, attr)
+}
+
+// addInterceptors is a helper function that validates and adds interceptors to
+// the current expression.
+func addInterceptors(interceptors []any, client bool) {
+	kind := "ServerInterceptor"
+	if client {
+		kind = "ClientInterceptor"
+	}
+	if len(interceptors) == 0 {
+		eval.ReportError("%s: at least one interceptor must be specified", kind)
+		return
+	}
+
+	var ints []*expr.InterceptorExpr
+	for _, i := range interceptors {
+		switch i := i.(type) {
+		case *expr.InterceptorExpr:
+			ints = append(ints, i)
+		case string:
+			if i == "" {
+				eval.ReportError("%s: interceptor name cannot be empty", kind)
+				return
+			}
+			var found bool
+			for _, in := range expr.Root.Interceptors {
+				if in.Name == i {
+					ints = append(ints, in)
+					found = true
+					break
+				}
+			}
+			if !found {
+				eval.ReportError("%s: interceptor %q not found", kind, i)
+			}
+		default:
+			eval.ReportError("%s: invalid interceptor %v", kind, i)
+		}
+	}
+
+	current := eval.Current()
+	switch actual := current.(type) {
+	case *expr.APIExpr:
+		if client {
+			actual.ClientInterceptors = append(actual.ClientInterceptors, ints...)
+		} else {
+			actual.ServerInterceptors = append(actual.ServerInterceptors, ints...)
+		}
+	case *expr.ServiceExpr:
+		if client {
+			actual.ClientInterceptors = append(actual.ClientInterceptors, ints...)
+		} else {
+			actual.ServerInterceptors = append(actual.ServerInterceptors, ints...)
+		}
+	case *expr.MethodExpr:
+		if client {
+			actual.ClientInterceptors = append(actual.ClientInterceptors, ints...)
+		} else {
+			actual.ServerInterceptors = append(actual.ServerInterceptors, ints...)
+		}
+	default:
+		eval.IncompatibleDSL()
+	}
+}
diff --git a/dsl/meta.go b/dsl/meta.go
index 05f917f8..60ffb196 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -288,3 +288,42 @@ func Meta(name string, value ...string) {
 		eval.IncompatibleDSL()
 	}
 }
+
+// RemoveMeta removes a meta key from an object.
+//
+// RemoveMeta may appear where Meta can appear.
+//
+// RemoveMeta takes a single argument, the name of the meta key to remove.
+func RemoveMeta(name string) {
+	switch e := eval.Current().(type) {
+	case *expr.APIExpr:
+		delete(e.Meta, name)
+	case *expr.ServerExpr:
+		delete(e.Meta, name)
+	case *expr.HostExpr:
+		delete(e.Meta, name)
+	case *expr.AttributeExpr:
+		delete(e.Meta, name)
+	case *expr.ResultTypeExpr:
+		delete(e.Meta, name)
+	case *expr.MethodExpr:
+		delete(e.Meta, name)
+	case *expr.ServiceExpr:
+		delete(e.Meta, name)
+	case *expr.HTTPServiceExpr:
+		delete(e.Meta, name)
+	case *expr.HTTPEndpointExpr:
+		delete(e.Meta, name)
+	case *expr.RouteExpr:
+		delete(e.Meta, name)
+	case *expr.HTTPFileServerExpr:
+		delete(e.Meta, name)
+	case *expr.HTTPResponseExpr:
+		delete(e.Meta, name)
+	case expr.CompositeExpr:
+		att := e.Attribute()
+		delete(att.Meta, name)
+	default:
+		eval.IncompatibleDSL()
+	}
+}
diff --git a/expr/api.go b/expr/api.go
index 01ed2026..7d77fb97 100644
--- a/expr/api.go
+++ b/expr/api.go
@@ -36,6 +36,10 @@ type (
 		// potentially multiple schemes. Incoming requests must validate
 		// at least one requirement to be authorized.
 		Requirements []*SecurityExpr
+		// ClientInterceptors is the list of API client interceptors.
+		ClientInterceptors []*InterceptorExpr
+		// ServerInterceptors is the list of API server interceptors.
+		ServerInterceptors []*InterceptorExpr
 		// HTTP contains the HTTP specific API level expressions.
 		HTTP *HTTPExpr
 		// GRPC contains the gRPC specific API level expressions.
diff --git a/expr/interceptor.go b/expr/interceptor.go
new file mode 100644
index 00000000..538d8546
--- /dev/null
+++ b/expr/interceptor.go
@@ -0,0 +1,84 @@
+package expr
+
+import (
+	"goa.design/goa/v3/eval"
+)
+
+type (
+	// InterceptorExpr describes an interceptor definition in the design.
+	// Interceptors are used to inject user code into the request/response processing pipeline.
+	// There are four kinds of interceptors, in order of execution:
+	//   * client-side payload: executes after the payload is encoded and before the request is sent to the server
+	//   * server-side request: executes after the request is decoded and before the payload is sent to the service
+	//   * server-side result: executes after the service returns a result and before the response is encoded
+	//   * client-side response: executes after the response is decoded and before the result is sent to the client
+	InterceptorExpr struct {
+		// Name is the name of the interceptor
+		Name string
+		// Description is the optional description of the interceptor
+		Description string
+		// ReadPayload lists the payload attribute names read by the interceptor
+		ReadPayload *AttributeExpr
+		// WritePayload lists the payload attribute names written by the interceptor
+		WritePayload *AttributeExpr
+		// ReadResult lists the result attribute names read by the interceptor
+		ReadResult *AttributeExpr
+		// WriteResult lists the result attribute names written by the interceptor
+		WriteResult *AttributeExpr
+	}
+)
+
+// EvalName returns the generic expression name used in error messages.
+func (i *InterceptorExpr) EvalName() string {
+	return "interceptor " + i.Name
+}
+
+// validate validates the interceptor.
+func (i *InterceptorExpr) validate(m *MethodExpr) *eval.ValidationErrors {
+	verr := new(eval.ValidationErrors)
+
+	if i.ReadPayload != nil || i.WritePayload != nil {
+		payloadObj := AsObject(m.Payload.Type)
+		if payloadObj == nil {
+			verr.Add(m, "interceptor %q cannot be applied because the method payload is not an object", i.Name)
+		}
+		if i.ReadPayload != nil {
+			i.validateAttributeAccess(m, "read payload", verr, payloadObj, i.ReadPayload)
+		}
+		if i.WritePayload != nil {
+			i.validateAttributeAccess(m, "write payload", verr, payloadObj, i.WritePayload)
+		}
+	}
+
+	if i.ReadResult != nil || i.WriteResult != nil {
+		if m.IsResultStreaming() {
+			verr.Add(m, "interceptor %q cannot be applied because the method result is streaming", i.Name)
+		}
+		resultObj := AsObject(m.Result.Type)
+		if resultObj == nil {
+			verr.Add(m, "interceptor %q cannot be applied because the method result is not an object", i.Name)
+		}
+		if i.ReadResult != nil {
+			i.validateAttributeAccess(m, "read result", verr, resultObj, i.ReadResult)
+		}
+		if i.WriteResult != nil {
+			i.validateAttributeAccess(m, "write result", verr, resultObj, i.WriteResult)
+		}
+	}
+
+	return verr
+}
+
+// validateAttributeAccess validates that all attributes in attr exist in obj
+func (i *InterceptorExpr) validateAttributeAccess(m *MethodExpr, source string, verr *eval.ValidationErrors, obj *Object, attr *AttributeExpr) {
+	attrObj := AsObject(attr.Type)
+	if attrObj == nil {
+		verr.Add(m, "interceptor %q %s attribute is not an object", i.Name, source)
+		return
+	}
+	for _, att := range *attrObj {
+		if obj.Attribute(att.Name) == nil {
+			verr.Add(m, "interceptor %q cannot %s attribute %q: attribute does not exist", i.Name, source, att.Name)
+		}
+	}
+}
diff --git a/expr/method.go b/expr/method.go
index 3ba80728..0183545e 100644
--- a/expr/method.go
+++ b/expr/method.go
@@ -32,6 +32,10 @@ type (
 		// schemes. Incoming requests must validate at least one
 		// requirement to be authorized.
 		Requirements []*SecurityExpr
+		// ClientInterceptors is the list of client interceptors.
+		ClientInterceptors []*InterceptorExpr
+		// ServerInterceptors is the list of server interceptors.
+		ServerInterceptors []*InterceptorExpr
 		// Service that owns method.
 		Service *ServiceExpr
 		// Meta is an arbitrary set of key/value pairs, see dsl.Meta
@@ -84,7 +88,8 @@ func (m *MethodExpr) EvalName() string {
 }
 
 // Prepare makes sure the payload and result types are initialized (to the Empty
-// type if nil).
+// type if nil) and merges the method interceptors with the API and service level
+// interceptors.
 func (m *MethodExpr) Prepare() {
 	if m.Payload == nil {
 		m.Payload = &AttributeExpr{Type: Empty}
@@ -97,11 +102,22 @@ func (m *MethodExpr) Prepare() {
 	}
 }
 
-// Validate validates the method payloads, results, and errors (if any).
+// Validate validates the method payloads, results, errors, security
+// requirements, and interceptors.
 func (m *MethodExpr) Validate() error {
 	verr := new(eval.ValidationErrors)
 	verr.Merge(m.Payload.Validate("payload", m))
-	// validate security scheme requirements
+	verr.Merge(m.StreamingPayload.Validate("streaming_payload", m))
+	verr.Merge(m.Result.Validate("result", m))
+	verr.Merge(m.validateRequirements())
+	verr.Merge(m.validateErrors())
+	verr.Merge(m.validateInterceptors())
+	return verr
+}
+
+// validateRequirements validates the security requirements.
+func (m *MethodExpr) validateRequirements() *eval.ValidationErrors {
+	verr := new(eval.ValidationErrors)
 	var requirements []*SecurityExpr
 	if len(m.Requirements) > 0 {
 		requirements = m.Requirements
@@ -185,12 +201,12 @@ func (m *MethodExpr) Validate() error {
 			verr.Add(m, "payload of method %q of service %q defines a OAuth2 access token attribute, but no OAuth2 security scheme exist", m.Name, m.Service.Name)
 		}
 	}
-	if m.StreamingPayload.Type != Empty {
-		verr.Merge(m.StreamingPayload.Validate("streaming_payload", m))
-	}
-	if m.Result.Type != Empty {
-		verr.Merge(m.Result.Validate("result", m))
-	}
+	return verr
+}
+
+// validateErrors validates the method errors.
+func (m *MethodExpr) validateErrors() *eval.ValidationErrors {
+	verr := new(eval.ValidationErrors)
 	for i, e := range m.Errors {
 		if err := e.Validate(); err != nil {
 			var verrs *eval.ValidationErrors
@@ -220,6 +236,44 @@ func (m *MethodExpr) Validate() error {
 	return verr
 }
 
+// validateInterceptors validates the method interceptors.
+func (m *MethodExpr) validateInterceptors() *eval.ValidationErrors {
+	verr := new(eval.ValidationErrors)
+	m.ClientInterceptors = mergeInterceptors(m.ClientInterceptors, m.Service.ClientInterceptors, Root.API.ClientInterceptors)
+	for _, i := range m.ClientInterceptors {
+		verr.Merge(i.validate(m))
+	}
+	m.ServerInterceptors = mergeInterceptors(m.ServerInterceptors, m.Service.ServerInterceptors, Root.API.ServerInterceptors)
+	for _, i := range m.ServerInterceptors {
+		verr.Merge(i.validate(m))
+	}
+	return verr
+}
+
+// mergeInterceptors merges interceptors from different levels (method, service, API)
+// while avoiding duplicates. The order of precedence is: method > service > API.
+func mergeInterceptors(methodLevel, serviceLevel, apiLevel []*InterceptorExpr) []*InterceptorExpr {
+	existing := make(map[string]struct{})
+	result := make([]*InterceptorExpr, 0, len(methodLevel)+len(serviceLevel)+len(apiLevel))
+
+	for _, i := range methodLevel {
+		existing[i.Name] = struct{}{}
+		result = append(result, i)
+	}
+	for _, i := range serviceLevel {
+		if _, ok := existing[i.Name]; !ok {
+			result = append(result, i)
+			existing[i.Name] = struct{}{}
+		}
+	}
+	for _, i := range apiLevel {
+		if _, ok := existing[i.Name]; !ok {
+			result = append(result, i)
+		}
+	}
+	return result
+}
+
 // hasTag is a helper function that traverses the given attribute and all its
 // bases recursively looking for an attribute with the given tag meta. This
 // recursion is only needed for attributes that have not been finalized yet.
diff --git a/expr/root.go b/expr/root.go
index 0240e54e..fb215c40 100644
--- a/expr/root.go
+++ b/expr/root.go
@@ -18,6 +18,8 @@ type (
 		API *APIExpr
 		// Services contains the list of services exposed by the API.
 		Services []*ServiceExpr
+		// Interceptors contains the list of interceptors.
+		Interceptors []*InterceptorExpr
 		// Errors contains the list of errors returned by all the API
 		// methods.
 		Errors []*ErrorExpr
diff --git a/expr/service.go b/expr/service.go
index 1a7d88e9..ab906c46 100644
--- a/expr/service.go
+++ b/expr/service.go
@@ -27,6 +27,10 @@ type (
 		// potentially multiple schemes. Incoming requests must validate
 		// at least one requirement to be authorized.
 		Requirements []*SecurityExpr
+		// ClientInterceptors is the list of client interceptors.
+		ClientInterceptors []*InterceptorExpr
+		// ServerInterceptors is the list of server interceptors.
+		ServerInterceptors []*InterceptorExpr
 		// Meta is a set of key/value pairs with semantic that is
 		// specific to each generator.
 		Meta MetaExpr
diff --git a/go.mod b/go.mod
index 17d9e52f..f2a5d754 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
 module goa.design/goa/v3
 
-go 1.22.0
+go 1.22.7
 
-toolchain go1.23.1
+toolchain go1.23.3
 
 require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
@@ -14,9 +14,9 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/text v0.21.0
-	golang.org/x/tools v0.28.0
-	google.golang.org/grpc v1.69.2
-	google.golang.org/protobuf v1.36.1
+	golang.org/x/tools v0.29.0
+	google.golang.org/grpc v1.69.4
+	google.golang.org/protobuf v1.36.3
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -32,8 +32,8 @@ require (
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
-	golang.org/x/net v0.32.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
 	golang.org/x/sync v0.10.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
 )
diff --git a/go.sum b/go.sum
index 7af0c27c..2c8593e4 100644
--- a/go.sum
+++ b/go.sum
@@ -64,22 +64,22 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY
 go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
 golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
 golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
-golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
-golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
-google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
-google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
-google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
-google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
+google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
+google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
+google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/grpc/codegen/client.go b/grpc/codegen/client.go
index fd77174a..f6669772 100644
--- a/grpc/codegen/client.go
+++ b/grpc/codegen/client.go
@@ -65,7 +65,7 @@ func client(genpkg string, svc *expr.GRPCServiceExpr) *codegen.File {
 			}
 		}
 		sections = append(sections, &codegen.SectionTemplate{
-			Name:   "client-init",
+			Name:   "grpc-client-init",
 			Source: readTemplate("client_init"),
 			Data:   data,
 		})
diff --git a/grpc/codegen/client_cli.go b/grpc/codegen/client_cli.go
index 275828a9..1135455c 100644
--- a/grpc/codegen/client_cli.go
+++ b/grpc/codegen/client_cli.go
@@ -80,6 +80,13 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da
 			Name: svcName + pbPkgName,
 		})
 		specs = append(specs, sd.Service.UserTypeImports...)
+		// Add interceptors import if service has client interceptors
+		if len(sd.Service.ClientInterceptors) > 0 {
+			specs = append(specs, &codegen.ImportSpec{
+				Path: genpkg + "/" + sd.Service.PathName,
+				Name: sd.Service.PkgName,
+			})
+		}
 	}
 
 	sections := []*codegen.SectionTemplate{
@@ -87,7 +94,7 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da
 		cli.UsageCommands(data),
 		cli.UsageExamples(data),
 		{
-			Name:   "parse-endpoint",
+			Name:   "parse-endpoint-grpc",
 			Source: readTemplate("parse_endpoint"),
 			Data: struct {
 				FlagsCode string
diff --git a/grpc/codegen/example_cli.go b/grpc/codegen/example_cli.go
index 36eee89f..8e346f5c 100644
--- a/grpc/codegen/example_cli.go
+++ b/grpc/codegen/example_cli.go
@@ -24,9 +24,10 @@ func ExampleCLIFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 
 // exampleCLI returns an example client tool HTTP implementation for the given
 // server expression.
-func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codegen.File {
+func exampleCLI(genpkg string, _ *expr.RootExpr, svr *expr.ServerExpr) *codegen.File {
 	var (
 		mainPath string
+		rootPath string
 
 		svrdata = example.Servers.Get(svr)
 	)
@@ -35,22 +36,11 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 		if _, err := os.Stat(mainPath); !os.IsNotExist(err) {
 			return nil // file already exists, skip it.
 		}
-	}
-
-	var (
-		rootPath string
-		apiPkg   string
-
-		scope = codegen.NewNameScope()
-	)
-	{
-		// genpkg is created by path.Join so the separator is / regardless of operating system
 		idx := strings.LastIndex(genpkg, string("/"))
 		rootPath = "."
 		if idx > 0 {
 			rootPath = genpkg[:idx]
 		}
-		apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
 	}
 
 	var (
@@ -68,11 +58,18 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 			{Path: "time"},
 			codegen.GoaImport(""),
 			codegen.GoaNamedImport("grpc", "goagrpc"),
-			{Path: rootPath, Name: apiPkg},
+			{Path: rootPath + "/interceptors"},
 			{Path: path.Join(genpkg, "grpc", "cli", svrdata.Dir), Name: "cli"},
 		}
 	}
 
+	var svcData []*ServiceData
+	for _, svc := range svr.Services {
+		if data := GRPCServices.Get(svc); data != nil {
+			svcData = append(svcData, data)
+		}
+	}
+
 	var (
 		sections []*codegen.SectionTemplate
 	)
@@ -82,7 +79,11 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 			{
 				Name:   "do-grpc-cli",
 				Source: readTemplate("do_grpc_cli"),
-				Data:   svrdata,
+				Data: map[string]any{
+					"DefaultTransport": svrdata.DefaultTransport(),
+					"Services":         svcData,
+					"InterceptorsPkg":  "interceptors",
+				},
 			},
 		}
 	}
diff --git a/grpc/codegen/templates/do_grpc_cli.go.tpl b/grpc/codegen/templates/do_grpc_cli.go.tpl
index 8802e20c..bdc52ca3 100644
--- a/grpc/codegen/templates/do_grpc_cli.go.tpl
+++ b/grpc/codegen/templates/do_grpc_cli.go.tpl
@@ -3,7 +3,19 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
     fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
   }
-	return cli.ParseEndpoint(conn)
+{{- range .Services }}
+	{{- if .Service.ClientInterceptors }}
+		{{ .Service.VarName }}Interceptors := {{ $.InterceptorsPkg }}.New{{ .Service.StructName }}ClientInterceptors()
+	{{- end }}
+{{- end }}
+	return cli.ParseEndpoint(
+		conn,
+{{- range .Services }}
+	{{- if .Service.ClientInterceptors }}
+		{{ .Service.VarName }}Interceptors,
+	{{- end }}
+{{- end }}
+	)
 }
 
 {{ if eq .DefaultTransport.Type "grpc" }}
diff --git a/grpc/codegen/templates/parse_endpoint.go.tpl b/grpc/codegen/templates/parse_endpoint.go.tpl
index b7f3eed3..43f98aec 100644
--- a/grpc/codegen/templates/parse_endpoint.go.tpl
+++ b/grpc/codegen/templates/parse_endpoint.go.tpl
@@ -1,6 +1,14 @@
 // ParseEndpoint returns the endpoint and payload as specified on the command
 // line.
-func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, any, error) {
+func ParseEndpoint(
+	cc *grpc.ClientConn,
+{{-  range .Commands }}
+	{{- if .Interceptors }}
+	{{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors,
+	{{- end }}
+{{- end }}
+	opts ...grpc.CallOption,
+) (goa.Endpoint, any, error) {
 	{{ .FlagsCode }}
 	var (
 		data     any
@@ -13,9 +21,14 @@ func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint,
 		case "{{ .Name }}":
 			c := {{ .PkgName }}.NewClient(cc, opts...)
 			switch epn {
-		{{- $pkgName := .PkgName }}{{ range .Subcommands }}
+		{{- $pkgName := .PkgName }}
+		{{- $interceptors := .Interceptors }}
+		{{ range .Subcommands }}
 			case "{{ .Name }}":
 				endpoint = c.{{ .MethodVarName }}()
+			{{- if $interceptors }}
+				endpoint = {{ $interceptors.PkgName }}.Wrap{{ .MethodVarName }}ClientEndpoint(endpoint, {{ $interceptors.VarName }})
+			{{- end }}
 			{{- if .BuildFunction }}
 				data, err = {{ $pkgName}}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }})
 			{{- else if .Conversion }}
diff --git a/grpc/codegen/client-no-server-pkgpath.golden b/grpc/codegen/testdata/client-no-server-pkgpath.golden
similarity index 91%
rename from grpc/codegen/client-no-server-pkgpath.golden
rename to grpc/codegen/testdata/client-no-server-pkgpath.golden
index 56ca0253..d1033172 100644
--- a/grpc/codegen/client-no-server-pkgpath.golden
+++ b/grpc/codegen/testdata/client-no-server-pkgpath.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/grpc/codegen/client-no-server.golden b/grpc/codegen/testdata/client-no-server.golden
similarity index 91%
rename from grpc/codegen/client-no-server.golden
rename to grpc/codegen/testdata/client-no-server.golden
index b6a7c341..e9259418 100644
--- a/grpc/codegen/client-no-server.golden
+++ b/grpc/codegen/testdata/client-no-server.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden b/grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden
similarity index 92%
rename from grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden
rename to grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden
index 22ea9249..5324d085 100644
--- a/grpc/codegen/client-server-hosting-multiple-services-pkgpath.golden
+++ b/grpc/codegen/testdata/client-server-hosting-multiple-services-pkgpath.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/grpc/codegen/client-server-hosting-multiple-services.golden b/grpc/codegen/testdata/client-server-hosting-multiple-services.golden
similarity index 91%
rename from grpc/codegen/client-server-hosting-multiple-services.golden
rename to grpc/codegen/testdata/client-server-hosting-multiple-services.golden
index 6956659b..dcfe3180 100644
--- a/grpc/codegen/client-server-hosting-multiple-services.golden
+++ b/grpc/codegen/testdata/client-server-hosting-multiple-services.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/grpc/codegen/client-server-hosting-service-subset-pkgpath.golden b/grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden
similarity index 92%
rename from grpc/codegen/client-server-hosting-service-subset-pkgpath.golden
rename to grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden
index 22ea9249..5324d085 100644
--- a/grpc/codegen/client-server-hosting-service-subset-pkgpath.golden
+++ b/grpc/codegen/testdata/client-server-hosting-service-subset-pkgpath.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/grpc/codegen/client-server-hosting-service-subset.golden b/grpc/codegen/testdata/client-server-hosting-service-subset.golden
similarity index 91%
rename from grpc/codegen/client-server-hosting-service-subset.golden
rename to grpc/codegen/testdata/client-server-hosting-service-subset.golden
index 6956659b..dcfe3180 100644
--- a/grpc/codegen/client-server-hosting-service-subset.golden
+++ b/grpc/codegen/testdata/client-server-hosting-service-subset.golden
@@ -13,5 +13,7 @@ func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
 	}
-	return cli.ParseEndpoint(conn)
+	return cli.ParseEndpoint(
+		conn,
+	)
 }
diff --git a/http/codegen/client.go b/http/codegen/client.go
index 52e02487..5d70b2ed 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -67,7 +67,7 @@ func clientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 	}
 
 	sections = append(sections, &codegen.SectionTemplate{
-		Name:    "client-init",
+		Name:    "http-client-init",
 		Source:  readTemplate("client_init"),
 		Data:    data,
 		FuncMap: map[string]any{"hasWebSocket": hasWebSocket},
diff --git a/http/codegen/client_cli.go b/http/codegen/client_cli.go
index 73a787e5..9cf67f28 100644
--- a/http/codegen/client_cli.go
+++ b/http/codegen/client_cli.go
@@ -127,6 +127,13 @@ func endpointParser(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, da
 			Path: genpkg + "/http/" + sd.Service.PathName + "/client",
 			Name: sd.Service.PkgName + "c",
 		})
+		// Add interceptors import if service has client interceptors
+		if len(sd.Service.ClientInterceptors) > 0 {
+			specs = append(specs, &codegen.ImportSpec{
+				Path: genpkg + "/" + sd.Service.PathName,
+				Name: sd.Service.PkgName,
+			})
+		}
 	}
 
 	cliData := make([]*cli.CommandData, len(data))
diff --git a/http/codegen/example_cli.go b/http/codegen/example_cli.go
index fe7b6e49..c6d57c83 100644
--- a/http/codegen/example_cli.go
+++ b/http/codegen/example_cli.go
@@ -7,6 +7,7 @@ import (
 
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/codegen/example"
+	"goa.design/goa/v3/codegen/service"
 	"goa.design/goa/v3/expr"
 )
 
@@ -30,20 +31,10 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 	if _, err := os.Stat(path); !os.IsNotExist(err) {
 		return nil // file already exists, skip it.
 	}
-	var (
-		rootPath string
-		apiPkg   string
-
-		scope = codegen.NewNameScope()
-	)
-	{
-		// genpkg is created by path.Join so the separator is / regardless of operating system
-		idx := strings.LastIndex(genpkg, string("/"))
-		rootPath = "."
-		if idx > 0 {
-			rootPath = genpkg[:idx]
-		}
-		apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
+	idx := strings.LastIndex(genpkg, string("/"))
+	rootPath := "."
+	if idx > 0 {
+		rootPath = genpkg[:idx]
 	}
 	specs := []*codegen.ImportSpec{
 		{Path: "context"},
@@ -59,8 +50,17 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 		codegen.GoaImport(""),
 		codegen.GoaNamedImport("http", "goahttp"),
 		{Path: genpkg + "/http/cli/" + svrdata.Dir, Name: "cli"},
-		{Path: rootPath, Name: apiPkg},
 	}
+	importScope := codegen.NewNameScope()
+	for _, svc := range root.Services {
+		data := service.Services.Get(svc.Name)
+		specs = append(specs, &codegen.ImportSpec{Path: genpkg + "/" + data.PkgName})
+		importScope.Unique(data.PkgName)
+	}
+	interceptorsPkg := importScope.Unique("interceptors", "ex")
+	specs = append(specs, &codegen.ImportSpec{Path: rootPath + "/interceptors", Name: interceptorsPkg})
+	apiPkg := importScope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
+	specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg})
 
 	var svcData []*ServiceData
 	for _, svc := range svr.Services {
@@ -73,6 +73,10 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 		{
 			Name:   "cli-http-start",
 			Source: readTemplate("cli_start"),
+			Data: map[string]any{
+				"Services":        svcData,
+				"InterceptorsPkg": interceptorsPkg,
+			},
 		},
 		{
 			Name:   "cli-http-streaming",
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index c1818960..b570c482 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -595,7 +595,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 	scope := codegen.NewNameScope()
 	scope.Unique("c") // 'c' is reserved as the client's receiver name.
 	scope.Unique("v") // 'v' is reserved as the request builder payload argument name.
-	rd := &ServiceData{
+	sd := &ServiceData{
 		Service:          svc,
 		ServerStruct:     "Server",
 		MountPointStruct: "MountPoint",
@@ -641,7 +641,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			VarName:      scope.Unique(codegen.Goify(s.FilePath, true)),
 			ArgName:      scope.Unique(fmt.Sprintf("fileSystem%s", codegen.Goify(s.FilePath, true))),
 		}
-		rd.FileServers = append(rd.FileServers, data)
+		sd.FileServers = append(sd.FileServers, data)
 	}
 
 	for _, httpEndpoint := range httpSvc.HTTPEndpoints {
@@ -672,10 +672,10 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 							// Path params may override requiredness, need to check payload.
 							pointer = httpEndpoint.MethodExpr.Payload.IsPrimitivePointer(arg, true)
 						}
-						name := rd.Scope.Name(codegen.Goify(arg, false))
+						name := sd.Scope.Name(codegen.Goify(arg, false))
 						var vcode string
 						if att.Validation != nil {
-							ctx := httpContext("", rd.Scope, true, false)
+							ctx := httpContext("", sd.Scope, true, false)
 							vcode = codegen.AttributeValidationCode(att, nil, ctx, true, expr.IsAlias(att.Type), name, arg)
 						}
 						initArgs[j] = &InitArgData{
@@ -686,8 +686,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 								Description: att.Description,
 								FieldName:   codegen.Goify(arg, true),
 								FieldType:   patt.Type,
-								TypeName:    rd.Scope.GoTypeName(att),
-								TypeRef:     rd.Scope.GoTypeRef(att),
+								TypeName:    sd.Scope.GoTypeName(att),
+								TypeRef:     sd.Scope.GoTypeRef(att),
 								Type:        att.Type,
 								Pointer:     pointer,
 								Required:    true,
@@ -727,7 +727,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			}
 		}
 
-		payload := buildPayloadData(httpEndpoint, rd)
+		payload := buildPayloadData(httpEndpoint, sd)
 
 		var (
 			reqs  service.RequirementsData
@@ -817,8 +817,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			ServiceVarName:  svc.VarName,
 			ServicePkgName:  svc.PkgName,
 			Payload:         payload,
-			Result:          buildResultData(httpEndpoint, rd),
-			Errors:          buildErrorsData(httpEndpoint, rd),
+			Result:          buildResultData(httpEndpoint, sd),
+			Errors:          buildErrorsData(httpEndpoint, sd),
 			HeaderSchemes:   hsch,
 			BodySchemes:     bosch,
 			QuerySchemes:    qsch,
@@ -837,7 +837,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			Requirements:    reqs,
 		}
 		if httpEndpoint.MethodExpr.IsStreaming() {
-			initWebSocketData(ed, httpEndpoint, rd)
+			initWebSocketData(ed, httpEndpoint, sd)
 		}
 
 		if httpEndpoint.MultipartRequest {
@@ -870,26 +870,26 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			}
 		}
 
-		rd.Endpoints = append(rd.Endpoints, ed)
+		sd.Endpoints = append(sd.Endpoints, ed)
 	}
 
 	for _, a := range httpSvc.HTTPEndpoints {
 		collectUserTypes(a.Body.Type, func(ut expr.UserType) {
-			if d := attributeTypeData(ut, true, true, true, rd); d != nil {
-				rd.ServerBodyAttributeTypes = append(rd.ServerBodyAttributeTypes, d)
+			if d := attributeTypeData(ut, true, true, true, sd); d != nil {
+				sd.ServerBodyAttributeTypes = append(sd.ServerBodyAttributeTypes, d)
 			}
-			if d := attributeTypeData(ut, true, false, false, rd); d != nil {
-				rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d)
+			if d := attributeTypeData(ut, true, false, false, sd); d != nil {
+				sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d)
 			}
 		})
 
 		if a.MethodExpr.StreamingPayload.Type != expr.Empty {
 			collectUserTypes(a.StreamingBody.Type, func(ut expr.UserType) {
-				if d := attributeTypeData(ut, true, true, true, rd); d != nil {
-					rd.ServerBodyAttributeTypes = append(rd.ServerBodyAttributeTypes, d)
+				if d := attributeTypeData(ut, true, true, true, sd); d != nil {
+					sd.ServerBodyAttributeTypes = append(sd.ServerBodyAttributeTypes, d)
 				}
-				if d := attributeTypeData(ut, true, false, false, rd); d != nil {
-					rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d)
+				if d := attributeTypeData(ut, true, false, false, sd); d != nil {
+					sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d)
 				}
 			})
 		}
@@ -900,8 +900,8 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 					// NOTE: ServerBodyAttributeTypes for response body types are
 					// collected in buildResponseBodyType because we have to generate
 					// body types for each view in a result type.
-					if d := attributeTypeData(ut, false, true, false, rd); d != nil {
-						rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d)
+					if d := attributeTypeData(ut, false, true, false, sd); d != nil {
+						sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d)
 					}
 				})
 			}
@@ -912,14 +912,14 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 				// NOTE: ServerBodyAttributeTypes for error response body types are
 				// collected in buildResponseBodyType because we have to generate
 				// body types for each view in a result type.
-				if d := attributeTypeData(ut, false, true, false, rd); d != nil {
-					rd.ClientBodyAttributeTypes = append(rd.ClientBodyAttributeTypes, d)
+				if d := attributeTypeData(ut, false, true, false, sd); d != nil {
+					sd.ClientBodyAttributeTypes = append(sd.ClientBodyAttributeTypes, d)
 				}
 			})
 		}
 	}
 
-	return rd
+	return sd
 }
 
 // makeHTTPType traverses the attribute recursively and performs these actions:
diff --git a/http/codegen/templates/cli_end.go.tpl b/http/codegen/templates/cli_end.go.tpl
index f94e8b78..31c52c9e 100644
--- a/http/codegen/templates/cli_end.go.tpl
+++ b/http/codegen/templates/cli_end.go.tpl
@@ -5,20 +5,25 @@ return cli.ParseEndpoint(
 		goahttp.RequestEncoder,
 		goahttp.ResponseDecoder,
 		debug,
-		{{- if needStream .Services }}
+{{- if needStream .Services }}
 		dialer,
-			{{- range $svc := .Services }}
-				{{- if hasWebSocket $svc }}
-				nil,
-				{{- end }}
-			{{- end }}
+	{{- range $svc := .Services }}
+		{{- if hasWebSocket $svc }}
+		nil,
 		{{- end }}
-		{{- range .Services }}
-			{{- range .Endpoints }}
-			  {{- if .MultipartRequestDecoder }}
+	{{- end }}
+{{- end }}
+{{- range .Services }}
+	{{- range .Endpoints }}
+		{{- if .MultipartRequestDecoder }}
 		{{ $.APIPkg }}.{{ .MultipartRequestEncoder.FuncName }},
-				{{- end }}
-			{{- end }}
 		{{- end }}
+	{{- end }}
+{{- end }}
+{{- range .Services }}
+	{{- if .Service.ClientInterceptors }}
+		{{ .Service.VarName }}Interceptors,
+	{{- end }}
+{{- end }}
 	)
-}
+}
\ No newline at end of file
diff --git a/http/codegen/templates/cli_start.go.tpl b/http/codegen/templates/cli_start.go.tpl
index c068dd6f..8ca80e51 100644
--- a/http/codegen/templates/cli_start.go.tpl
+++ b/http/codegen/templates/cli_start.go.tpl
@@ -1,10 +1,20 @@
 func doHTTP(scheme, host string, timeout int, debug bool) (goa.Endpoint, any, error) {
 	var (
 		doer goahttp.Doer
+{{- range .Services }}
+	{{- if .Service.ClientInterceptors }}
+		{{ .Service.VarName }}Interceptors {{ .Service.PkgName }}.ClientInterceptors
+	{{- end }}
+{{- end }}
 	)
 	{
 		doer = &http.Client{Timeout: time.Duration(timeout) * time.Second}
 		if debug {
 			doer = goahttp.NewDebugDoer(doer)
 		}
+{{- range .Services }}
+	{{- if .Service.ClientInterceptors }}
+		{{ .Service.VarName }}Interceptors = {{ $.InterceptorsPkg }}.New{{ .Service.StructName }}ClientInterceptors()
+	{{- end }}
+{{- end }}
 	}
diff --git a/http/codegen/templates/parse_endpoint.go.tpl b/http/codegen/templates/parse_endpoint.go.tpl
index d967bcfd..fc552795 100644
--- a/http/codegen/templates/parse_endpoint.go.tpl
+++ b/http/codegen/templates/parse_endpoint.go.tpl
@@ -14,12 +14,15 @@ func ParseEndpoint(
 			{{- end }}
 		{{- end }}
 	{{- end }}
-	{{- range $c := .Commands }}
+	{{- range $i, $c := .Commands }}
 	{{- range .Subcommands }}
 		{{- if .MultipartVarName }}
 	{{ .MultipartVarName }} {{ $c.PkgName }}.{{ .MultipartFuncName }},
 		{{- end }}
 	{{- end }}
+	{{- if .Interceptors }}
+	{{ .Interceptors.VarName }} {{ .Interceptors.PkgName }}.ClientInterceptors,
+	{{- end }}
 	{{- end }}
 ) (goa.Endpoint, any, error) {
 	{{ .FlagsCode }}
@@ -34,11 +37,16 @@ func ParseEndpoint(
 		case "{{ .Name }}":
 			c := {{ .PkgName }}.NewClient(scheme, host, doer, enc, dec, restore{{ if .NeedStream }}, dialer, {{ .VarName }}Configurer{{ end }})
 			switch epn {
-		{{- $pkgName := .PkgName }}{{ range .Subcommands }}
+		{{- $pkgName := .PkgName }}
+		{{- $interceptors := .Interceptors }}
+		{{- range .Subcommands }}
 			case "{{ .Name }}":
 				endpoint = c.{{ .MethodVarName }}({{ if .MultipartVarName }}{{ .MultipartVarName }}{{ end }})
+			{{- if $interceptors }}
+				endpoint = {{ $interceptors.PkgName }}.Wrap{{ .MethodVarName }}ClientEndpoint(endpoint, {{ $interceptors.VarName }})
+			{{- end }}
 			{{- if .BuildFunction }}
-				data, err = {{ $pkgName}}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }})
+				data, err = {{ $pkgName }}.{{ .BuildFunction.Name }}({{ range .BuildFunction.ActualParams }}*{{ . }}Flag, {{ end }})
 			{{- else if .Conversion }}
 				{{ .Conversion }}
 			{{- end }}
diff --git a/pkg/interceptor.go b/pkg/interceptor.go
new file mode 100644
index 00000000..c784f14b
--- /dev/null
+++ b/pkg/interceptor.go
@@ -0,0 +1,17 @@
+package goa
+
+type (
+	// InterceptorInfo contains information about the request shared between
+	// all interceptors in the service chain. It provides access to the service name,
+	// method name, endpoint function, and request payload.
+	InterceptorInfo struct {
+		// Name of service handling request
+		Service string
+		// Name of method handling request
+		Method string
+		// Endpoint of request, can be used for retrying
+		Endpoint Endpoint
+		// Payload of request
+		RawPayload any
+	}
+)


Test patch:
diff --git a/codegen/service/client_test.go b/codegen/service/client_test.go
index 2fa51523..6cfda7d4 100644
--- a/codegen/service/client_test.go
+++ b/codegen/service/client_test.go
@@ -32,6 +32,7 @@ func TestClient(t *testing.T) {
 		{"client-streaming-payload-no-result", testdata.StreamingPayloadNoResultMethodDSL, testdata.StreamingPayloadNoResultMethodClient},
 		{"client-bidirectional-streaming", testdata.BidirectionalStreamingMethodDSL, testdata.BidirectionalStreamingMethodClient},
 		{"client-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethodClient},
+		{"client-interceptor", testdata.EndpointWithClientInterceptorDSL, testdata.InterceptorClient},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/codegen/service/convert_test.go b/codegen/service/convert_test.go
index 52a0e1d2..479d3c1c 100644
--- a/codegen/service/convert_test.go
+++ b/codegen/service/convert_test.go
@@ -14,7 +14,6 @@ import (
 	"goa.design/goa/v3/codegen"
 	"goa.design/goa/v3/codegen/service/testdata"
 	"goa.design/goa/v3/dsl"
-	"goa.design/goa/v3/eval"
 	"goa.design/goa/v3/expr"
 )
 
@@ -257,30 +256,7 @@ func TestConvertFile(t *testing.T) {
 	}
 }
 
-// runDSL returns the DSL root resulting from running the given DSL.
-func runDSL(t *testing.T, dsl func()) *expr.RootExpr {
-	// reset all roots and codegen data structures
-	Services = make(ServicesData)
-	eval.Reset()
-	expr.Root = new(expr.RootExpr)
-	expr.GeneratedResultTypes = new(expr.ResultTypesRoot)
-	require.NoError(t, eval.Register(expr.Root))
-	require.NoError(t, eval.Register(expr.GeneratedResultTypes))
-	expr.Root.API = expr.NewAPIExpr("test api", func() {})
-	expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()}
-
-	// run DSL (first pass)
-	require.True(t, eval.Execute(dsl, nil))
-
-	// run DSL (second pass)
-	require.NoError(t, eval.RunDSL())
-
-	// return generated root
-	return expr.Root
-}
-
 // Test fixtures
-
 var obj = &expr.UserTypeExpr{
 	AttributeExpr: &expr.AttributeExpr{
 		Type: &expr.Object{
diff --git a/codegen/service/endpoint_test.go b/codegen/service/endpoint_test.go
index 0356bf95..2132eeee 100644
--- a/codegen/service/endpoint_test.go
+++ b/codegen/service/endpoint_test.go
@@ -34,6 +34,8 @@ func TestEndpoint(t *testing.T) {
 		{"endpoint-streaming-payload-no-result", testdata.StreamingPayloadNoResultMethodDSL, testdata.StreamingPayloadNoResultMethodEndpoint},
 		{"endpoint-bidirectional-streaming", testdata.BidirectionalStreamingEndpointDSL, testdata.BidirectionalStreamingMethodEndpoint},
 		{"endpoint-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethodEndpoint},
+		{"endpoint-with-server-interceptor", testdata.EndpointWithServerInterceptorDSL, testdata.EndpointWithServerInterceptor},
+		{"endpoint-with-multiple-interceptors", testdata.EndpointWithMultipleInterceptorsDSL, testdata.EndpointWithMultipleInterceptors},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/codegen/service/example_interceptors_test.go b/codegen/service/example_interceptors_test.go
new file mode 100644
index 00000000..37b8aa42
--- /dev/null
+++ b/codegen/service/example_interceptors_test.go
@@ -0,0 +1,119 @@
+package service
+
+import (
+	"bytes"
+	"go/format"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"goa.design/goa/v3/codegen/service/testdata"
+	"goa.design/goa/v3/expr"
+)
+
+func TestExampleInterceptorsFiles(t *testing.T) {
+	cases := []struct {
+		Name          string
+		DSL           func()
+		ExpectedFiles []string
+	}{
+		{
+			Name: "no-interceptors",
+			DSL:  testdata.NoInterceptorExampleDSL,
+		},
+		{
+			Name: "server-interceptor",
+			DSL:  testdata.ServerInterceptorExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "server_interceptor_service_server.go"),
+			},
+		},
+		{
+			Name: "client-interceptor",
+			DSL:  testdata.ClientInterceptorExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "client_interceptor_service_client.go"),
+			},
+		},
+		{
+			Name: "server-interceptor-by-name",
+			DSL:  testdata.ServerInterceptorByNameExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "server_interceptor_by_name_service_server.go"),
+			},
+		},
+		{
+			Name: "multiple-interceptors",
+			DSL:  testdata.MultipleInterceptorsExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "multiple_interceptors_service_server.go"),
+				filepath.Join("interceptors", "multiple_interceptors_service_client.go"),
+			},
+		},
+		{
+			Name: "multiple-services",
+			DSL:  testdata.MultipleServicesInterceptorsExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "multiple_services_interceptors_service_server.go"),
+				filepath.Join("interceptors", "multiple_services_interceptors_service_client.go"),
+				filepath.Join("interceptors", "multiple_services_interceptors_service2_server.go"),
+				filepath.Join("interceptors", "multiple_services_interceptors_service2_client.go"),
+			},
+		},
+		{
+			Name: "api-interceptors",
+			DSL:  testdata.APIInterceptorExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "api_interceptor_service_server.go"),
+				filepath.Join("interceptors", "api_interceptor_service_client.go"),
+			},
+		},
+		{
+			Name: "chained-interceptors",
+			DSL:  testdata.ChainedInterceptorExampleDSL,
+			ExpectedFiles: []string{
+				filepath.Join("interceptors", "chained_interceptor_service_server.go"),
+				filepath.Join("interceptors", "chained_interceptor_service_client.go"),
+			},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			// Run DSL
+			root := expr.RunDSL(t, c.DSL)
+			require.NotNil(t, root)
+
+			// Generate files
+			fs := ExampleInterceptorsFiles("", root)
+			require.Len(t, fs, len(c.ExpectedFiles))
+
+			// Verify file paths
+			paths := make([]string, len(fs))
+			for i, f := range fs {
+				paths[i] = f.Path
+			}
+			assert.ElementsMatch(t, c.ExpectedFiles, paths)
+
+			// Verify file content
+			for _, f := range fs {
+				buf := new(bytes.Buffer)
+				for _, s := range f.SectionTemplates {
+					require.NoError(t, s.Write(buf))
+				}
+				bs, err := format.Source(buf.Bytes())
+				require.NoError(t, err, buf.String())
+				code := string(bs)
+
+				// Use the base name of the generated file without extension for the golden file
+				baseName := filepath.Base(f.Path)
+				ext := filepath.Ext(baseName)
+				goldenName := baseName[:len(baseName)-len(ext)] + ".golden"
+				golden := filepath.Join("testdata", "example_interceptors", goldenName)
+				compareOrUpdateGolden(t, code, golden)
+			}
+		})
+	}
+}
diff --git a/codegen/service/interceptors_test.go b/codegen/service/interceptors_test.go
new file mode 100644
index 00000000..6fa7c3ab
--- /dev/null
+++ b/codegen/service/interceptors_test.go
@@ -0,0 +1,206 @@
+package service
+
+import (
+	"bytes"
+	"flag"
+	"go/format"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/codegen/service/testdata"
+	"goa.design/goa/v3/expr"
+)
+
+var updateGolden = false
+
+func init() {
+	flag.BoolVar(&updateGolden, "w", false, "update golden files")
+}
+
+func TestInterceptors(t *testing.T) {
+	cases := []struct {
+		Name              string
+		DSL               func()
+		expectedFileCount int
+	}{
+		{"no-interceptors", testdata.NoInterceptorsDSL, 0},
+		{"single-api-server-interceptor", testdata.SingleAPIServerInterceptorDSL, 2},
+		{"single-service-server-interceptor", testdata.SingleServiceServerInterceptorDSL, 2},
+		{"single-method-server-interceptor", testdata.SingleMethodServerInterceptorDSL, 2},
+		{"single-client-interceptor", testdata.SingleClientInterceptorDSL, 2},
+		{"multiple-interceptors", testdata.MultipleInterceptorsExampleDSL, 3},
+		{"interceptor-with-read-payload", testdata.InterceptorWithReadPayloadDSL, 3},
+		{"interceptor-with-write-payload", testdata.InterceptorWithWritePayloadDSL, 3},
+		{"interceptor-with-read-write-payload", testdata.InterceptorWithReadWritePayloadDSL, 3},
+		{"interceptor-with-read-result", testdata.InterceptorWithReadResultDSL, 3},
+		{"interceptor-with-write-result", testdata.InterceptorWithWriteResultDSL, 3},
+		{"interceptor-with-read-write-result", testdata.InterceptorWithReadWriteResultDSL, 3},
+		{"streaming-interceptors", testdata.StreamingInterceptorsDSL, 2},
+		{"streaming-interceptors-with-read-payload", testdata.StreamingInterceptorsWithReadPayloadDSL, 2},
+		{"streaming-interceptors-with-read-result", testdata.StreamingInterceptorsWithReadResultDSL, 2},
+	}
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			root := runDSL(t, c.DSL)
+			require.Len(t, root.Services, 1)
+
+			fs := InterceptorsFiles("goa.design/goa/example", root.Services[0])
+
+			require.Len(t, fs, c.expectedFileCount)
+			for _, f := range fs {
+				buf := new(bytes.Buffer)
+				for _, s := range f.SectionTemplates[1:] {
+					require.NoError(t, s.Write(buf))
+				}
+				bs, err := format.Source(buf.Bytes())
+				require.NoError(t, err, buf.String())
+				code := strings.ReplaceAll(string(bs), "\r\n", "\n")
+
+				golden := filepath.Join("testdata", "interceptors", c.Name+"_"+filepath.Base(f.Path)+".golden")
+				compareOrUpdateGolden(t, code, golden)
+			}
+		})
+	}
+}
+
+func TestInvalidInterceptors(t *testing.T) {
+	cases := []struct {
+		Name        string
+		DSL         func()
+		ErrContains string
+	}{
+		{
+			Name:        "streaming-result-interceptor",
+			DSL:         testdata.StreamingResultInterceptorDSL,
+			ErrContains: "cannot be applied because the method result is streaming",
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			_, err := runDSLWithError(t, c.DSL)
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), c.ErrContains)
+		})
+	}
+}
+
+func TestCollectAttributes(t *testing.T) {
+	cases := []struct {
+		name      string
+		attrNames *expr.AttributeExpr
+		parent    *expr.AttributeExpr
+		want      []*AttributeData
+	}{
+		{
+			name:      "nil-attributes",
+			attrNames: nil,
+			parent:    &expr.AttributeExpr{Type: &expr.Object{}},
+			want:      nil,
+		},
+		{
+			name:      "non-object-attributes",
+			attrNames: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)},
+			parent:    &expr.AttributeExpr{Type: &expr.Object{}},
+			want:      nil,
+		},
+		{
+			name: "simple-string-attribute",
+			attrNames: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+				},
+			},
+			parent: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+				},
+				Validation: &expr.ValidationExpr{Required: []string{"name"}},
+			},
+			want: []*AttributeData{
+				{Name: "Name", TypeRef: "string", Pointer: false},
+			},
+		},
+		{
+			name: "pointer-primitive",
+			attrNames: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind)}},
+				},
+			},
+			parent: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind), Meta: map[string][]string{"struct:field:pointer": {"true"}}}},
+				},
+			},
+			want: []*AttributeData{
+				{Name: "Age", TypeRef: "int", Pointer: true},
+			},
+		},
+		{
+			name: "multiple-attributes",
+			attrNames: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+					{Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind)}},
+				},
+			},
+			parent: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+					{Name: "age", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.IntKind), Meta: map[string][]string{"struct:field:pointer": {"true"}}}},
+				},
+				Validation: &expr.ValidationExpr{Required: []string{"name"}},
+			},
+			want: []*AttributeData{
+				{Name: "Name", TypeRef: "string", Pointer: false},
+				{Name: "Age", TypeRef: "int", Pointer: true},
+			},
+		},
+		{
+			name: "attribute-not-in-parent",
+			attrNames: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "missing", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+				},
+			},
+			parent: &expr.AttributeExpr{
+				Type: &expr.Object{
+					{Name: "name", Attribute: &expr.AttributeExpr{Type: expr.Primitive(expr.StringKind)}},
+				},
+				Validation: &expr.ValidationExpr{Required: []string{"name"}},
+			},
+			want: []*AttributeData{nil},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			scope := codegen.NewNameScope()
+			got := collectAttributes(tc.attrNames, tc.parent, scope)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}
+
+func compareOrUpdateGolden(t *testing.T, code, golden string) {
+	t.Helper()
+	if updateGolden {
+		require.NoError(t, os.MkdirAll(filepath.Dir(golden), 0750))
+		require.NoError(t, os.WriteFile(golden, []byte(code), 0640))
+		return
+	}
+	data, err := os.ReadFile(golden)
+	require.NoError(t, err)
+	if runtime.GOOS == "windows" {
+		data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
+	}
+	assert.Equal(t, string(data), code)
+}
diff --git a/codegen/service/templates/service_interceptor.go.tpl b/codegen/service/templates/service_interceptor.go.tpl
new file mode 100644
index 00000000..e69de29b
diff --git a/codegen/service/testdata/client_code.go b/codegen/service/testdata/client_code.go
index 2b235613..a13cacde 100644
--- a/codegen/service/testdata/client_code.go
+++ b/codegen/service/testdata/client_code.go
@@ -283,3 +283,28 @@ func (c *Client) BidirectionalStreamingNoPayloadMethod(ctx context.Context) (res
 	return ires.(BidirectionalStreamingNoPayloadMethodClientStream), nil
 }
 `
+
+const InterceptorClient = `// Client is the "ServiceWithClientInterceptor" service client.
+type Client struct {
+	MethodEndpoint goa.Endpoint
+}
+
+// NewClient initializes a "ServiceWithClientInterceptor" service client given
+// the endpoints.
+func NewClient(method goa.Endpoint, ci ClientInterceptors) *Client {
+	return &Client{
+		MethodEndpoint: WrapMethodClientEndpoint(method, ci),
+	}
+}
+
+// Method calls the "Method" endpoint of the "ServiceWithClientInterceptor"
+// service.
+func (c *Client) Method(ctx context.Context, p string) (res string, err error) {
+	var ires any
+	ires, err = c.MethodEndpoint(ctx, p)
+	if err != nil {
+		return
+	}
+	return ires.(string), nil
+}
+`
diff --git a/codegen/service/testdata/endpoint_code.go b/codegen/service/testdata/endpoint_code.go
index 978ff5e5..324d104a 100644
--- a/codegen/service/testdata/endpoint_code.go
+++ b/codegen/service/testdata/endpoint_code.go
@@ -513,3 +513,101 @@ func NewBidirectionalStreamingNoPayloadMethodEndpoint(s Service) goa.Endpoint {
 	}
 }
 `
+
+var EndpointWithServerInterceptor = `// Endpoints wraps the "ServiceWithServerInterceptor" service endpoints.
+type Endpoints struct {
+	Method goa.Endpoint
+}
+
+// NewEndpoints wraps the methods of the "ServiceWithServerInterceptor" service
+// with endpoints.
+func NewEndpoints(s Service, si ServerInterceptors) *Endpoints {
+	endpoints := &Endpoints{
+		Method: NewMethodEndpoint(s),
+	}
+	endpoints.Method = WrapMethodEndpoint(endpoints.Method, si)
+	return endpoints
+}
+
+// Use applies the given middleware to all the "ServiceWithServerInterceptor"
+// service endpoints.
+func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) {
+	e.Method = m(e.Method)
+}
+
+// NewMethodEndpoint returns an endpoint function that calls the method
+// "Method" of service "ServiceWithServerInterceptor".
+func NewMethodEndpoint(s Service) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		p := req.(string)
+		return s.Method(ctx, p)
+	}
+}
+`
+
+var EndpointWithMultipleInterceptors = `// Endpoints wraps the "ServiceWithMultipleInterceptors" service endpoints.
+type Endpoints struct {
+	Method goa.Endpoint
+}
+
+// NewEndpoints wraps the methods of the "ServiceWithMultipleInterceptors"
+// service with endpoints.
+func NewEndpoints(s Service, si ServerInterceptors) *Endpoints {
+	endpoints := &Endpoints{
+		Method: NewMethodEndpoint(s),
+	}
+	endpoints.Method = WrapMethodEndpoint(endpoints.Method, si)
+	return endpoints
+}
+
+// Use applies the given middleware to all the
+// "ServiceWithMultipleInterceptors" service endpoints.
+func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) {
+	e.Method = m(e.Method)
+}
+
+// NewMethodEndpoint returns an endpoint function that calls the method
+// "Method" of service "ServiceWithMultipleInterceptors".
+func NewMethodEndpoint(s Service) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		p := req.(string)
+		return s.Method(ctx, p)
+	}
+}
+`
+
+var EndpointStreamingWithInterceptor = `// Endpoints wraps the "ServiceStreamingWithInterceptor" service endpoints.
+type Endpoints struct {
+	Method goa.Endpoint
+}
+
+// MethodEndpointInput holds both the payload and the server stream of the
+// "Method" method.
+type MethodEndpointInput struct {
+	// Stream is the server stream used by the "Method" method to send data.
+	Stream MethodServerStream
+}
+
+// NewEndpoints wraps the methods of the "ServiceStreamingWithInterceptor" service
+// with endpoints.
+func NewEndpoints(s Service, i ServerInterceptors) *Endpoints {
+	return &Endpoints{
+		Method: WrapMethodEndpoint(NewMethodEndpoint(s), i),
+	}
+}
+
+// Use applies the given middleware to all the "ServiceStreamingWithInterceptor"
+// service endpoints.
+func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) {
+	e.Method = m(e.Method)
+}
+
+// NewMethodEndpoint returns an endpoint function that calls the method "Method"
+// of service "ServiceStreamingWithInterceptor".
+func NewMethodEndpoint(s Service) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		ep := req.(*MethodEndpointInput)
+		return nil, s.Method(ctx, ep.Stream)
+	}
+}
+`
diff --git a/codegen/service/testdata/endpoint_dsls.go b/codegen/service/testdata/endpoint_dsls.go
index 4ca2c7b4..de3a2204 100644
--- a/codegen/service/testdata/endpoint_dsls.go
+++ b/codegen/service/testdata/endpoint_dsls.go
@@ -181,3 +181,47 @@ var BidirectionalStreamingEndpointDSL = func() {
 		})
 	})
 }
+
+var EndpointWithServerInterceptorDSL = func() {
+	Interceptor("logging")
+	Service("ServiceWithServerInterceptor", func() {
+		Method("Method", func() {
+			ServerInterceptor("logging")
+			Payload(String)
+			Result(String)
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+}
+
+var EndpointWithMultipleInterceptorsDSL = func() {
+	Interceptor("logging")
+	Interceptor("metrics")
+	Service("ServiceWithMultipleInterceptors", func() {
+		Method("Method", func() {
+			ServerInterceptor("logging")
+			ServerInterceptor("metrics")
+			Payload(String)
+			Result(String)
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+}
+
+var EndpointStreamingWithInterceptorDSL = func() {
+	Interceptor("logging")
+	Service("ServiceStreamingWithInterceptor", func() {
+		Method("Method", func() {
+			ServerInterceptor("logging")
+			StreamingPayload(String)
+			StreamingResult(String)
+			HTTP(func() {
+				GET("/")
+			})
+		})
+	})
+}
diff --git a/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden
new file mode 100644
index 00000000..5188cca3
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/api_interceptor_service_client.golden
@@ -0,0 +1,35 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// APIInterceptorService example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	apiinterceptorservice "api_interceptor_service"
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+)
+
+// APIInterceptorServiceClientInterceptors implements the client interceptors for the APIInterceptorService service.
+type APIInterceptorServiceClientInterceptors struct {
+}
+
+// NewAPIInterceptorServiceClientInterceptors creates a new client interceptor for the APIInterceptorService service.
+func NewAPIInterceptorServiceClientInterceptors() *APIInterceptorServiceClientInterceptors {
+	return &APIInterceptorServiceClientInterceptors{}
+}
+func (i *APIInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[API] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[API] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[API] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden
new file mode 100644
index 00000000..ff86a4ce
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/api_interceptor_service_server.golden
@@ -0,0 +1,35 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// APIInterceptorService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	apiinterceptorservice "api_interceptor_service"
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+)
+
+// APIInterceptorServiceServerInterceptors implements the server interceptor for the APIInterceptorService service.
+type APIInterceptorServiceServerInterceptors struct {
+}
+
+// NewAPIInterceptorServiceServerInterceptors creates a new server interceptor for the APIInterceptorService service.
+func NewAPIInterceptorServiceServerInterceptors() *APIInterceptorServiceServerInterceptors {
+	return &APIInterceptorServiceServerInterceptors{}
+}
+func (i *APIInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[API] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[API] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[API] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden
new file mode 100644
index 00000000..4a1eeebc
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_client.golden
@@ -0,0 +1,55 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// ChainedInterceptorService example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	chainedinterceptorservice "chained_interceptor_service"
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+)
+
+// ChainedInterceptorServiceClientInterceptors implements the client interceptors for the ChainedInterceptorService service.
+type ChainedInterceptorServiceClientInterceptors struct {
+}
+
+// NewChainedInterceptorServiceClientInterceptors creates a new client interceptor for the ChainedInterceptorService service.
+func NewChainedInterceptorServiceClientInterceptors() *ChainedInterceptorServiceClientInterceptors {
+	return &ChainedInterceptorServiceClientInterceptors{}
+}
+func (i *ChainedInterceptorServiceClientInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[API] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[API] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[API] Received response: %v", resp)
+	return resp, nil
+}
+func (i *ChainedInterceptorServiceClientInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Method] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Method] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Method] Received response: %v", resp)
+	return resp, nil
+}
+func (i *ChainedInterceptorServiceClientInterceptors) Service(ctx context.Context, info *interceptors.ServiceInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Service] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Service] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Service] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden
new file mode 100644
index 00000000..4027c3a5
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/chained_interceptor_service_server.golden
@@ -0,0 +1,55 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// ChainedInterceptorService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	chainedinterceptorservice "chained_interceptor_service"
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+)
+
+// ChainedInterceptorServiceServerInterceptors implements the server interceptor for the ChainedInterceptorService service.
+type ChainedInterceptorServiceServerInterceptors struct {
+}
+
+// NewChainedInterceptorServiceServerInterceptors creates a new server interceptor for the ChainedInterceptorService service.
+func NewChainedInterceptorServiceServerInterceptors() *ChainedInterceptorServiceServerInterceptors {
+	return &ChainedInterceptorServiceServerInterceptors{}
+}
+func (i *ChainedInterceptorServiceServerInterceptors) API(ctx context.Context, info *interceptors.APIInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[API] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[API] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[API] Response: %v", resp)
+	return resp, nil
+}
+func (i *ChainedInterceptorServiceServerInterceptors) Method(ctx context.Context, info *interceptors.MethodInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Method] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Method] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Method] Response: %v", resp)
+	return resp, nil
+}
+func (i *ChainedInterceptorServiceServerInterceptors) Service(ctx context.Context, info *interceptors.ServiceInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Service] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Service] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Service] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden b/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden
new file mode 100644
index 00000000..f64ea4bd
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/client_interceptor_service_client.golden
@@ -0,0 +1,35 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// ClientInterceptorService example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	clientinterceptorservice "client_interceptor_service"
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+)
+
+// ClientInterceptorServiceClientInterceptors implements the client interceptors for the ClientInterceptorService service.
+type ClientInterceptorServiceClientInterceptors struct {
+}
+
+// NewClientInterceptorServiceClientInterceptors creates a new client interceptor for the ClientInterceptorService service.
+func NewClientInterceptorServiceClientInterceptors() *ClientInterceptorServiceClientInterceptors {
+	return &ClientInterceptorServiceClientInterceptors{}
+}
+func (i *ClientInterceptorServiceClientInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden
new file mode 100644
index 00000000..3086ebce
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_client.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleInterceptorsService example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleinterceptorsservice "multiple_interceptors_service"
+)
+
+// MultipleInterceptorsServiceClientInterceptors implements the client interceptors for the MultipleInterceptorsService service.
+type MultipleInterceptorsServiceClientInterceptors struct {
+}
+
+// NewMultipleInterceptorsServiceClientInterceptors creates a new client interceptor for the MultipleInterceptorsService service.
+func NewMultipleInterceptorsServiceClientInterceptors() *MultipleInterceptorsServiceClientInterceptors {
+	return &MultipleInterceptorsServiceClientInterceptors{}
+}
+func (i *MultipleInterceptorsServiceClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test2] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test2] Received response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleInterceptorsServiceClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test4] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test4] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden
new file mode 100644
index 00000000..75f774ef
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_interceptors_service_server.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleInterceptorsService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleinterceptorsservice "multiple_interceptors_service"
+)
+
+// MultipleInterceptorsServiceServerInterceptors implements the server interceptor for the MultipleInterceptorsService service.
+type MultipleInterceptorsServiceServerInterceptors struct {
+}
+
+// NewMultipleInterceptorsServiceServerInterceptors creates a new server interceptor for the MultipleInterceptorsService service.
+func NewMultipleInterceptorsServiceServerInterceptors() *MultipleInterceptorsServiceServerInterceptors {
+	return &MultipleInterceptorsServiceServerInterceptors{}
+}
+func (i *MultipleInterceptorsServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleInterceptorsServiceServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test3] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test3] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden
new file mode 100644
index 00000000..ff9d8403
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_client.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleServicesInterceptorsService2 example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleservicesinterceptorsservice2 "multiple_services_interceptors_service2"
+)
+
+// MultipleServicesInterceptorsService2ClientInterceptors implements the client interceptors for the MultipleServicesInterceptorsService2 service.
+type MultipleServicesInterceptorsService2ClientInterceptors struct {
+}
+
+// NewMultipleServicesInterceptorsService2ClientInterceptors creates a new client interceptor for the MultipleServicesInterceptorsService2 service.
+func NewMultipleServicesInterceptorsService2ClientInterceptors() *MultipleServicesInterceptorsService2ClientInterceptors {
+	return &MultipleServicesInterceptorsService2ClientInterceptors{}
+}
+func (i *MultipleServicesInterceptorsService2ClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test2] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test2] Received response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleServicesInterceptorsService2ClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test4] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test4] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden
new file mode 100644
index 00000000..173144f0
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service2_server.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleServicesInterceptorsService2 example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleservicesinterceptorsservice2 "multiple_services_interceptors_service2"
+)
+
+// MultipleServicesInterceptorsService2ServerInterceptors implements the server interceptor for the MultipleServicesInterceptorsService2 service.
+type MultipleServicesInterceptorsService2ServerInterceptors struct {
+}
+
+// NewMultipleServicesInterceptorsService2ServerInterceptors creates a new server interceptor for the MultipleServicesInterceptorsService2 service.
+func NewMultipleServicesInterceptorsService2ServerInterceptors() *MultipleServicesInterceptorsService2ServerInterceptors {
+	return &MultipleServicesInterceptorsService2ServerInterceptors{}
+}
+func (i *MultipleServicesInterceptorsService2ServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleServicesInterceptorsService2ServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test3] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test3] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden
new file mode 100644
index 00000000..72c48c6b
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_client.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleServicesInterceptorsService example client interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleservicesinterceptorsservice "multiple_services_interceptors_service"
+)
+
+// MultipleServicesInterceptorsServiceClientInterceptors implements the client interceptors for the MultipleServicesInterceptorsService service.
+type MultipleServicesInterceptorsServiceClientInterceptors struct {
+}
+
+// NewMultipleServicesInterceptorsServiceClientInterceptors creates a new client interceptor for the MultipleServicesInterceptorsService service.
+func NewMultipleServicesInterceptorsServiceClientInterceptors() *MultipleServicesInterceptorsServiceClientInterceptors {
+	return &MultipleServicesInterceptorsServiceClientInterceptors{}
+}
+func (i *MultipleServicesInterceptorsServiceClientInterceptors) Test2(ctx context.Context, info *interceptors.Test2Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test2] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test2] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test2] Received response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleServicesInterceptorsServiceClientInterceptors) Test4(ctx context.Context, info *interceptors.Test4Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test4] Sending request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test4] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test4] Received response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden
new file mode 100644
index 00000000..4e780407
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/multiple_services_interceptors_service_server.golden
@@ -0,0 +1,45 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// MultipleServicesInterceptorsService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	multipleservicesinterceptorsservice "multiple_services_interceptors_service"
+)
+
+// MultipleServicesInterceptorsServiceServerInterceptors implements the server interceptor for the MultipleServicesInterceptorsService service.
+type MultipleServicesInterceptorsServiceServerInterceptors struct {
+}
+
+// NewMultipleServicesInterceptorsServiceServerInterceptors creates a new server interceptor for the MultipleServicesInterceptorsService service.
+func NewMultipleServicesInterceptorsServiceServerInterceptors() *MultipleServicesInterceptorsServiceServerInterceptors {
+	return &MultipleServicesInterceptorsServiceServerInterceptors{}
+}
+func (i *MultipleServicesInterceptorsServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Response: %v", resp)
+	return resp, nil
+}
+func (i *MultipleServicesInterceptorsServiceServerInterceptors) Test3(ctx context.Context, info *interceptors.Test3Info, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test3] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test3] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test3] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden b/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden
new file mode 100644
index 00000000..2660f90a
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/server_interceptor_by_name_service_server.golden
@@ -0,0 +1,35 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// ServerInterceptorByNameService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	serverinterceptorbynameservice "server_interceptor_by_name_service"
+)
+
+// ServerInterceptorByNameServiceServerInterceptors implements the server interceptor for the ServerInterceptorByNameService service.
+type ServerInterceptorByNameServiceServerInterceptors struct {
+}
+
+// NewServerInterceptorByNameServiceServerInterceptors creates a new server interceptor for the ServerInterceptorByNameService service.
+func NewServerInterceptorByNameServiceServerInterceptors() *ServerInterceptorByNameServiceServerInterceptors {
+	return &ServerInterceptorByNameServiceServerInterceptors{}
+}
+func (i *ServerInterceptorByNameServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden b/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden
new file mode 100644
index 00000000..517fdbc2
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors/server_interceptor_service_server.golden
@@ -0,0 +1,35 @@
+// Code generated by goa v3.19.1, DO NOT EDIT.
+//
+// ServerInterceptorService example server interceptors
+//
+// Command:
+// goa
+
+package interceptors
+
+import (
+	"context"
+	"fmt"
+	"goa.design/clue/log"
+	goa "goa.design/goa/v3/pkg"
+	serverinterceptorservice "server_interceptor_service"
+)
+
+// ServerInterceptorServiceServerInterceptors implements the server interceptor for the ServerInterceptorService service.
+type ServerInterceptorServiceServerInterceptors struct {
+}
+
+// NewServerInterceptorServiceServerInterceptors creates a new server interceptor for the ServerInterceptorService service.
+func NewServerInterceptorServiceServerInterceptors() *ServerInterceptorServiceServerInterceptors {
+	return &ServerInterceptorServiceServerInterceptors{}
+}
+func (i *ServerInterceptorServiceServerInterceptors) Test(ctx context.Context, info *interceptors.TestInfo, next goa.Endpoint) (any, error) {
+	log.Printf(ctx, "[Test] Processing request: %v", info.RawPayload)
+	resp, err := next(ctx, info.RawPayload)
+	if err != nil {
+		log.Printf(ctx, "[Test] Error: %v", err)
+		return nil, err
+	}
+	log.Printf(ctx, "[Test] Response: %v", resp)
+	return resp, nil
+}
diff --git a/codegen/service/testdata/example_interceptors_dsls.go b/codegen/service/testdata/example_interceptors_dsls.go
new file mode 100644
index 00000000..42ecbb21
--- /dev/null
+++ b/codegen/service/testdata/example_interceptors_dsls.go
@@ -0,0 +1,96 @@
+package testdata
+
+import (
+	. "goa.design/goa/v3/dsl"
+)
+
+var NoInterceptorExampleDSL = func() {
+	var _ = Service("NoInterceptorService", func() {
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var ServerInterceptorExampleDSL = func() {
+	var SInterceptor = Interceptor("test")
+	var _ = Service("ServerInterceptorService", func() {
+		ServerInterceptor(SInterceptor)
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var ClientInterceptorExampleDSL = func() {
+	var CInterceptor = Interceptor("test")
+	var _ = Service("ClientInterceptorService", func() {
+		ClientInterceptor(CInterceptor)
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var ServerInterceptorByNameExampleDSL = func() {
+	var _ = Interceptor("test")
+	var _ = Service("ServerInterceptorByNameService", func() {
+		ServerInterceptor("test")
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var MultipleInterceptorsExampleDSL = func() {
+	var _ = Interceptor("test")
+	var _ = Interceptor("test2")
+	var SInterceptor = Interceptor("test3")
+	var CInterceptor = Interceptor("test4")
+	var _ = Service("MultipleInterceptorsService", func() {
+		ServerInterceptor("test", SInterceptor)
+		ClientInterceptor("test2", CInterceptor)
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var MultipleServicesInterceptorsExampleDSL = func() {
+	var _ = Interceptor("test")
+	var _ = Interceptor("test2")
+	var SInterceptor = Interceptor("test3")
+	var CInterceptor = Interceptor("test4")
+	var _ = Service("MultipleServicesInterceptorsService", func() {
+		ServerInterceptor("test", SInterceptor)
+		ClientInterceptor("test2", CInterceptor)
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+	var _ = Service("MultipleServicesInterceptorsService2", func() {
+		ServerInterceptor("test", SInterceptor)
+		ClientInterceptor("test2", CInterceptor)
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var APIInterceptorExampleDSL = func() {
+	var APIInterceptor = Interceptor("api")
+	var _ = API("test", func() {
+		ServerInterceptor(APIInterceptor)
+		ClientInterceptor(APIInterceptor)
+	})
+	var _ = Service("APIInterceptorService", func() {
+		Method("Method", func() { HTTP(func() { GET("/") }) })
+	})
+}
+
+var ChainedInterceptorExampleDSL = func() {
+	var APIInterceptor = Interceptor("api")
+	var ServiceInterceptor = Interceptor("service")
+	var MethodInterceptor = Interceptor("method")
+
+	var _ = API("test", func() {
+		ServerInterceptor(APIInterceptor)
+		ClientInterceptor(APIInterceptor)
+	})
+
+	var _ = Service("ChainedInterceptorService", func() {
+		ServerInterceptor(ServiceInterceptor)
+		ClientInterceptor(ServiceInterceptor)
+		Method("Method", func() {
+			ServerInterceptor(MethodInterceptor)
+			ClientInterceptor(MethodInterceptor)
+			HTTP(func() { GET("/") })
+		})
+	})
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden
new file mode 100644
index 00000000..5b21e6d3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodvalidation(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..1f4ac6eb
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_interceptor_wrappers.go.golden
@@ -0,0 +1,29 @@
+
+
+// wrapValidationMethod applies the validation server interceptor to endpoints.
+func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithReadPayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
+// wrapClientValidationMethod applies the validation client interceptor to
+// endpoints.
+func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithReadPayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden
new file mode 100644
index 00000000..7e616c94
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-payload_service_interceptors.go.golden
@@ -0,0 +1,48 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// ValidationInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	ValidationInfo goa.InterceptorInfo
+
+	// ValidationPayload provides type-safe access to the method payload.
+	// It allows reading and writing specific fields of the payload as defined
+	// in the design.
+	ValidationPayload interface {
+		Name() string
+	}
+)
+
+// Private implementation types
+type (
+	validationMethodPayload struct {
+		payload *MethodPayload
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodvalidation(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+
+// Payload returns a type-safe accessor for the method payload.
+func (info *ValidationInfo) Payload() ValidationPayload {
+	return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)}
+}
+
+// Private implementation methods
+
+func (p *validationMethodPayload) Name() string {
+	return p.payload.Name
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden
new file mode 100644
index 00000000..7bf02718
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodcaching(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..5f86fe84
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_interceptor_wrappers.go.golden
@@ -0,0 +1,28 @@
+
+
+// wrapCachingMethod applies the caching server interceptor to endpoints.
+func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithReadResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
+// wrapClientCachingMethod applies the caching client interceptor to endpoints.
+func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithReadResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden
new file mode 100644
index 00000000..a658f22d
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-result_service_interceptors.go.golden
@@ -0,0 +1,47 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// CachingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	CachingInfo goa.InterceptorInfo
+
+	// CachingResult provides type-safe access to the method result.
+	// It allows reading and writing specific fields of the result as defined
+	// in the design.
+	CachingResult interface {
+		Data() string
+	}
+)
+
+// Private implementation types
+type (
+	cachingMethodResult struct {
+		result *MethodResult
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodcaching(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+// Result returns a type-safe accessor for the method result.
+func (info *CachingInfo) Result(res any) CachingResult {
+	return &cachingMethodResult{result: res.(*MethodResult)}
+}
+
+// Private implementation methods
+
+func (r *cachingMethodResult) Data() string {
+	return r.result.Data
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden
new file mode 100644
index 00000000..5b21e6d3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodvalidation(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..f628ed5e
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_interceptor_wrappers.go.golden
@@ -0,0 +1,29 @@
+
+
+// wrapValidationMethod applies the validation server interceptor to endpoints.
+func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithReadWritePayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
+// wrapClientValidationMethod applies the validation client interceptor to
+// endpoints.
+func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithReadWritePayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden
new file mode 100644
index 00000000..0f84e223
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-payload_service_interceptors.go.golden
@@ -0,0 +1,52 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// ValidationInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	ValidationInfo goa.InterceptorInfo
+
+	// ValidationPayload provides type-safe access to the method payload.
+	// It allows reading and writing specific fields of the payload as defined
+	// in the design.
+	ValidationPayload interface {
+		Name() string
+		SetName(string)
+	}
+)
+
+// Private implementation types
+type (
+	validationMethodPayload struct {
+		payload *MethodPayload
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodvalidation(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+
+// Payload returns a type-safe accessor for the method payload.
+func (info *ValidationInfo) Payload() ValidationPayload {
+	return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)}
+}
+
+// Private implementation methods
+
+func (p *validationMethodPayload) Name() string {
+	return p.payload.Name
+}
+func (p *validationMethodPayload) SetName(v string) {
+	p.payload.Name = v
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden
new file mode 100644
index 00000000..7bf02718
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodcaching(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..1e5ab953
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_interceptor_wrappers.go.golden
@@ -0,0 +1,28 @@
+
+
+// wrapCachingMethod applies the caching server interceptor to endpoints.
+func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithReadWriteResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
+// wrapClientCachingMethod applies the caching client interceptor to endpoints.
+func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithReadWriteResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden
new file mode 100644
index 00000000..96c934c2
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-read-write-result_service_interceptors.go.golden
@@ -0,0 +1,51 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// CachingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	CachingInfo goa.InterceptorInfo
+
+	// CachingResult provides type-safe access to the method result.
+	// It allows reading and writing specific fields of the result as defined
+	// in the design.
+	CachingResult interface {
+		Data() string
+		SetData(string)
+	}
+)
+
+// Private implementation types
+type (
+	cachingMethodResult struct {
+		result *MethodResult
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodcaching(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+// Result returns a type-safe accessor for the method result.
+func (info *CachingInfo) Result(res any) CachingResult {
+	return &cachingMethodResult{result: res.(*MethodResult)}
+}
+
+// Private implementation methods
+
+func (r *cachingMethodResult) Data() string {
+	return r.result.Data
+}
+func (r *cachingMethodResult) SetData(v string) {
+	r.result.Data = v
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden
new file mode 100644
index 00000000..5b21e6d3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodvalidation(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..70657d6e
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_interceptor_wrappers.go.golden
@@ -0,0 +1,29 @@
+
+
+// wrapValidationMethod applies the validation server interceptor to endpoints.
+func wrapMethodValidation(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithWritePayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
+// wrapClientValidationMethod applies the validation client interceptor to
+// endpoints.
+func wrapClientMethodValidation(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &ValidationInfo{
+			Service:    "InterceptorWithWritePayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Validation(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden
new file mode 100644
index 00000000..c76eb125
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-payload_service_interceptors.go.golden
@@ -0,0 +1,48 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Validation(ctx context.Context, info *ValidationInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// ValidationInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	ValidationInfo goa.InterceptorInfo
+
+	// ValidationPayload provides type-safe access to the method payload.
+	// It allows reading and writing specific fields of the payload as defined
+	// in the design.
+	ValidationPayload interface {
+		SetName(string)
+	}
+)
+
+// Private implementation types
+type (
+	validationMethodPayload struct {
+		payload *MethodPayload
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodvalidation(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+
+// Payload returns a type-safe accessor for the method payload.
+func (info *ValidationInfo) Payload() ValidationPayload {
+	return &validationMethodPayload{payload: info.RawPayload.(*MethodPayload)}
+}
+
+// Private implementation methods
+
+func (p *validationMethodPayload) SetName(v string) {
+	p.payload.Name = v
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden
new file mode 100644
index 00000000..7bf02718
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_client_interceptors.go.golden
@@ -0,0 +1,14 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodcaching(endpoint, i)
+	return endpoint
+}
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..bfcca2ab
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_interceptor_wrappers.go.golden
@@ -0,0 +1,28 @@
+
+
+// wrapCachingMethod applies the caching server interceptor to endpoints.
+func wrapMethodCaching(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithWriteResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
+// wrapClientCachingMethod applies the caching client interceptor to endpoints.
+func wrapClientMethodCaching(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &CachingInfo{
+			Service:    "InterceptorWithWriteResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Caching(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden
new file mode 100644
index 00000000..26b2620a
--- /dev/null
+++ b/codegen/service/testdata/interceptors/interceptor-with-write-result_service_interceptors.go.golden
@@ -0,0 +1,47 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Caching(ctx context.Context, info *CachingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// CachingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	CachingInfo goa.InterceptorInfo
+
+	// CachingResult provides type-safe access to the method result.
+	// It allows reading and writing specific fields of the result as defined
+	// in the design.
+	CachingResult interface {
+		SetData(string)
+	}
+)
+
+// Private implementation types
+type (
+	cachingMethodResult struct {
+		result *MethodResult
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodcaching(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+// Result returns a type-safe accessor for the method result.
+func (info *CachingInfo) Result(res any) CachingResult {
+	return &cachingMethodResult{result: res.(*MethodResult)}
+}
+
+// Private implementation methods
+
+func (r *cachingMethodResult) SetData(v string) {
+	r.result.Data = v
+}
diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden
new file mode 100644
index 00000000..00d3b745
--- /dev/null
+++ b/codegen/service/testdata/interceptors/multiple-interceptors_client_interceptors.go.golden
@@ -0,0 +1,27 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Test2(ctx context.Context, info *Test2Info, next goa.Endpoint) (any, error)
+	Test4(ctx context.Context, info *Test4Info, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// Test2Info provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	Test2Info goa.InterceptorInfo
+	// Test4Info provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	Test4Info goa.InterceptorInfo
+)
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodtest2(endpoint, i)
+	endpoint = wrapClientMethodtest4(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..f23c4d71
--- /dev/null
+++ b/codegen/service/testdata/interceptors/multiple-interceptors_interceptor_wrappers.go.golden
@@ -0,0 +1,54 @@
+
+
+// wrapTestMethod applies the test server interceptor to endpoints.
+func wrapMethodTest(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &TestInfo{
+			Service:    "MultipleInterceptorsService",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Test(ctx, info, endpoint)
+	}
+}
+
+// wrapTest3Method applies the test3 server interceptor to endpoints.
+func wrapMethodTest3(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &Test3Info{
+			Service:    "MultipleInterceptorsService",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Test3(ctx, info, endpoint)
+	}
+}
+
+// wrapClientTest2Method applies the test2 client interceptor to endpoints.
+func wrapClientMethodTest2(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &Test2Info{
+			Service:    "MultipleInterceptorsService",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Test2(ctx, info, endpoint)
+	}
+}
+
+// wrapClientTest4Method applies the test4 client interceptor to endpoints.
+func wrapClientMethodTest4(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &Test4Info{
+			Service:    "MultipleInterceptorsService",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Test4(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden
new file mode 100644
index 00000000..1df6e00c
--- /dev/null
+++ b/codegen/service/testdata/interceptors/multiple-interceptors_service_interceptors.go.golden
@@ -0,0 +1,27 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Test(ctx context.Context, info *TestInfo, next goa.Endpoint) (any, error)
+	Test3(ctx context.Context, info *Test3Info, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// TestInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	TestInfo goa.InterceptorInfo
+	// Test3Info provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	Test3Info goa.InterceptorInfo
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodtest(endpoint, i)
+	endpoint = wrapMethodtest3(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..b7e0eebf
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_interceptor_wrappers.go.golden
@@ -0,0 +1,27 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "SingleAPIServerInterceptor",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
+
+// wrapLoggingMethod2 applies the logging server interceptor to endpoints.
+func wrapMethod2Logging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "SingleAPIServerInterceptor",
+			Method:     "Method2",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden
new file mode 100644
index 00000000..f4870a7e
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-api-server-interceptor_service_interceptors.go.golden
@@ -0,0 +1,29 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
+// WrapMethod2Endpoint wraps the Method2 endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethod2logging(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden
new file mode 100644
index 00000000..8f272d85
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-client-interceptor_client_interceptors.go.golden
@@ -0,0 +1,22 @@
+// ClientInterceptors defines the interface for all client-side interceptors.
+// Client interceptors execute after the payload is encoded and before the request
+// is sent to the server. The implementation is responsible for calling next to
+// complete the request.
+type ClientInterceptors interface {
+	Tracing(ctx context.Context, info *TracingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// TracingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	TracingInfo goa.InterceptorInfo
+)
+
+// WrapMethodClientEndpoint wraps the Method endpoint with the client
+// interceptors defined in the design.
+func WrapMethodClientEndpoint(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	endpoint = wrapClientMethodtracing(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..b05666e3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-client-interceptor_interceptor_wrappers.go.golden
@@ -0,0 +1,15 @@
+
+
+// wrapClientTracingMethod applies the tracing client interceptor to endpoints.
+func wrapClientMethodTracing(endpoint goa.Endpoint, i ClientInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &TracingInfo{
+			Service:    "SingleClientInterceptor",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Tracing(ctx, info, endpoint)
+	}
+}
+
diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..c06dbb3e
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_interceptor_wrappers.go.golden
@@ -0,0 +1,14 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "SingleMethodServerInterceptor",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden
new file mode 100644
index 00000000..58b647a3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-method-server-interceptor_service_interceptors.go.golden
@@ -0,0 +1,22 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..c15ab439
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_interceptor_wrappers.go.golden
@@ -0,0 +1,27 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "SingleServerInterceptor",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
+
+// wrapLoggingMethod2 applies the logging server interceptor to endpoints.
+func wrapMethod2Logging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "SingleServerInterceptor",
+			Method:     "Method2",
+			Endpoint:   endpoint,
+			RawPayload: req,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden
new file mode 100644
index 00000000..f4870a7e
--- /dev/null
+++ b/codegen/service/testdata/interceptors/single-service-server-interceptor_service_interceptors.go.golden
@@ -0,0 +1,29 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
+// WrapMethod2Endpoint wraps the Method2 endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethod2Endpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethod2logging(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..554dade0
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_interceptor_wrappers.go.golden
@@ -0,0 +1,14 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "StreamingInterceptorsWithReadPayload",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req.(*MethodServerStream).Payload,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden
new file mode 100644
index 00000000..4a173a3c
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-payload_service_interceptors.go.golden
@@ -0,0 +1,52 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+
+	// LoggingPayload provides type-safe access to the method payload.
+	// It allows reading and writing specific fields of the payload as defined
+	// in the design.
+	LoggingPayload interface {
+		Initial() string
+	}
+)
+
+// Private implementation types
+type (
+	loggingMethodPayload struct {
+		payload *MethodPayload
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+
+// Payload returns a type-safe accessor for the method payload.
+func (info *LoggingInfo) Payload() LoggingPayload {
+	return &loggingMethodPayload{payload: info.RawPayload.(*MethodPayload)}
+}
+
+// Private implementation methods
+
+func (p *loggingMethodPayload) Initial() string {
+	if p.payload.Initial == nil {
+		var zero string
+		return zero
+	}
+	return *p.payload.Initial
+}
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..2fc769d4
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_interceptor_wrappers.go.golden
@@ -0,0 +1,14 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "StreamingInterceptorsWithReadResult",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req.(*MethodServerStream).Payload,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden
new file mode 100644
index 00000000..fc61ae62
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors-with-read-result_service_interceptors.go.golden
@@ -0,0 +1,51 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+
+	// LoggingResult provides type-safe access to the method result.
+	// It allows reading and writing specific fields of the result as defined
+	// in the design.
+	LoggingResult interface {
+		Data() string
+	}
+)
+
+// Private implementation types
+type (
+	loggingMethodResult struct {
+		result *MethodResult
+	}
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
+// Public accessor methods for Info types
+// Result returns a type-safe accessor for the method result.
+func (info *LoggingInfo) Result(res any) LoggingResult {
+	return &loggingMethodResult{result: res.(*MethodResult)}
+}
+
+// Private implementation methods
+
+func (r *loggingMethodResult) Data() string {
+	if r.result.Data == nil {
+		var zero string
+		return zero
+	}
+	return *r.result.Data
+}
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden
new file mode 100644
index 00000000..0361aa23
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors_interceptor_wrappers.go.golden
@@ -0,0 +1,14 @@
+
+
+// wrapLoggingMethod applies the logging server interceptor to endpoints.
+func wrapMethodLogging(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	return func(ctx context.Context, req any) (any, error) {
+		info := &LoggingInfo{
+			Service:    "StreamingInterceptors",
+			Method:     "Method",
+			Endpoint:   endpoint,
+			RawPayload: req.(*MethodServerStream).Payload,
+		}
+		return i.Logging(ctx, info, endpoint)
+	}
+}
\ No newline at end of file
diff --git a/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden
new file mode 100644
index 00000000..58b647a3
--- /dev/null
+++ b/codegen/service/testdata/interceptors/streaming-interceptors_service_interceptors.go.golden
@@ -0,0 +1,22 @@
+// ServerInterceptors defines the interface for all server-side interceptors.
+// Server interceptors execute after the request is decoded and before the
+// payload is sent to the service. The implementation is responsible for calling
+// next to complete the request.
+type ServerInterceptors interface {
+	Logging(ctx context.Context, info *LoggingInfo, next goa.Endpoint) (any, error)
+}
+
+// Access interfaces for interceptor payloads and results
+type (
+	// LoggingInfo provides metadata about the current interception.
+	// It includes service name, method name, and access to the endpoint.
+	LoggingInfo goa.InterceptorInfo
+)
+
+// WrapMethodEndpoint wraps the Method endpoint with the server-side
+// interceptors defined in the design.
+func WrapMethodEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
+	endpoint = wrapMethodlogging(endpoint, i)
+	return endpoint
+}
+
diff --git a/codegen/service/testdata/interceptors_dsls.go b/codegen/service/testdata/interceptors_dsls.go
new file mode 100644
index 00000000..dead879c
--- /dev/null
+++ b/codegen/service/testdata/interceptors_dsls.go
@@ -0,0 +1,297 @@
+package testdata
+
+import (
+	. "goa.design/goa/v3/dsl"
+)
+
+var NoInterceptorsDSL = func() {
+	Service("NoInterceptors", func() {
+		Method("Method", func() {
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var SingleAPIServerInterceptorDSL = func() {
+	Interceptor("logging")
+	API("SingleAPIServerInterceptor", func() {
+		ServerInterceptor("logging")
+	})
+	Service("SingleAPIServerInterceptor", func() {
+		Method("Method", func() {
+			HTTP(func() { GET("/1") })
+		})
+		Method("Method2", func() {
+			HTTP(func() { GET("/2") })
+		})
+	})
+}
+
+var SingleServiceServerInterceptorDSL = func() {
+	Interceptor("logging")
+	Service("SingleServerInterceptor", func() {
+		ServerInterceptor("logging")
+		Method("Method", func() {
+			HTTP(func() {
+				GET("/1")
+			})
+		})
+		Method("Method2", func() {
+			HTTP(func() {
+				GET("/2")
+			})
+		})
+	})
+}
+
+var SingleMethodServerInterceptorDSL = func() {
+	Interceptor("logging")
+	Service("SingleMethodServerInterceptor", func() {
+		Method("Method", func() {
+			ServerInterceptor("logging")
+			HTTP(func() { GET("/1") })
+		})
+		Method("Method2", func() {
+			HTTP(func() { GET("/2") })
+		})
+	})
+}
+
+var SingleClientInterceptorDSL = func() {
+	Interceptor("tracing")
+	Service("SingleClientInterceptor", func() {
+		ClientInterceptor("tracing")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("id", Int)
+			})
+			Result(func() {
+				Attribute("value", String)
+			})
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var MultipleInterceptorsDSL = func() {
+	Interceptor("logging")
+	Interceptor("tracing")
+	Interceptor("metrics")
+	Service("MultipleInterceptors", func() {
+		ServerInterceptor("logging")
+		ServerInterceptor("tracing")
+		ClientInterceptor("metrics")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("query", String)
+			})
+			Result(func() {
+				Attribute("data", String)
+			})
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var InterceptorWithReadPayloadDSL = func() {
+	Interceptor("validation", func() {
+		ReadPayload(func() {
+			Attribute("name")
+		})
+	})
+	Service("InterceptorWithReadPayload", func() {
+		ServerInterceptor("validation")
+		ClientInterceptor("validation")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("name", String)
+				Required("name")
+			})
+			HTTP(func() { POST("/") })
+		})
+	})
+}
+
+var InterceptorWithWritePayloadDSL = func() {
+	Interceptor("validation", func() {
+		WritePayload(func() {
+			Attribute("name")
+		})
+	})
+	Service("InterceptorWithWritePayload", func() {
+		ServerInterceptor("validation")
+		ClientInterceptor("validation")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("name", String)
+				Required("name")
+			})
+			HTTP(func() { POST("/") })
+		})
+	})
+}
+
+var InterceptorWithReadWritePayloadDSL = func() {
+	Interceptor("validation", func() {
+		ReadPayload(func() {
+			Attribute("name")
+		})
+		WritePayload(func() {
+			Attribute("name")
+		})
+	})
+	Service("InterceptorWithReadWritePayload", func() {
+		ServerInterceptor("validation")
+		ClientInterceptor("validation")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("name", String)
+				Required("name")
+			})
+			HTTP(func() { POST("/") })
+		})
+	})
+}
+
+var InterceptorWithReadResultDSL = func() {
+	Interceptor("caching", func() {
+		ReadResult(func() {
+			Attribute("data")
+		})
+	})
+	Service("InterceptorWithReadResult", func() {
+		ServerInterceptor("caching")
+		ClientInterceptor("caching")
+		Method("Method", func() {
+			Result(func() {
+				Attribute("data", String)
+				Required("data")
+			})
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var InterceptorWithWriteResultDSL = func() {
+	Interceptor("caching", func() {
+		WriteResult(func() {
+			Attribute("data")
+		})
+	})
+	Service("InterceptorWithWriteResult", func() {
+		ServerInterceptor("caching")
+		ClientInterceptor("caching")
+		Method("Method", func() {
+			Result(func() {
+				Attribute("data", String)
+				Required("data")
+			})
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var InterceptorWithReadWriteResultDSL = func() {
+	Interceptor("caching", func() {
+		ReadResult(func() {
+			Attribute("data")
+		})
+		WriteResult(func() {
+			Attribute("data")
+		})
+	})
+	Service("InterceptorWithReadWriteResult", func() {
+		ServerInterceptor("caching")
+		ClientInterceptor("caching")
+		Method("Method", func() {
+			Result(func() {
+				Attribute("data", String)
+				Required("data")
+			})
+			HTTP(func() { GET("/") })
+		})
+	})
+}
+
+var StreamingInterceptorsDSL = func() {
+	Interceptor("logging")
+	Service("StreamingInterceptors", func() {
+		ServerInterceptor("logging")
+		Method("Method", func() {
+			StreamingPayload(func() {
+				Attribute("chunk", String)
+			})
+			StreamingResult(func() {
+				Attribute("data", String)
+			})
+			HTTP(func() { GET("/stream") })
+		})
+	})
+}
+
+var StreamingInterceptorsWithReadPayloadDSL = func() {
+	Interceptor("logging", func() {
+		ReadPayload(func() {
+			Attribute("initial")
+		})
+	})
+	Service("StreamingInterceptorsWithReadPayload", func() {
+		ServerInterceptor("logging")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("initial", String)
+			})
+			StreamingPayload(func() {
+				Attribute("chunk", String)
+			})
+			HTTP(func() {
+				Header("initial")
+				GET("/stream")
+			})
+		})
+	})
+}
+
+var StreamingInterceptorsWithReadResultDSL = func() {
+	Interceptor("logging", func() {
+		ReadResult(func() {
+			Attribute("data")
+		})
+	})
+	Service("StreamingInterceptorsWithReadResult", func() {
+		ServerInterceptor("logging")
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("initial", String)
+			})
+			StreamingPayload(func() {
+				Attribute("chunk", String)
+			})
+			Result(func() {
+				Attribute("data", String)
+			})
+			HTTP(func() {
+				Header("initial")
+				GET("/stream")
+			})
+		})
+	})
+}
+
+// Invalid DSL
+var StreamingResultInterceptorDSL = func() {
+	Interceptor("logging", func() {
+		ReadResult(func() {
+			Attribute("data")
+		})
+	})
+	Service("StreamingResultInterceptor", func() {
+		ServerInterceptor("logging")
+		Method("Method", func() {
+			StreamingResult(func() {
+				Attribute("data", String)
+			})
+			HTTP(func() { GET("/stream") })
+		})
+	})
+}
diff --git a/codegen/service/testdata/service_dsls.go b/codegen/service/testdata/service_dsls.go
index 5fd9b06b..76f7f917 100644
--- a/codegen/service/testdata/service_dsls.go
+++ b/codegen/service/testdata/service_dsls.go
@@ -967,3 +967,17 @@ var PkgPathPayloadAttributeDSL = func() {
 		})
 	})
 }
+
+var EndpointWithClientInterceptorDSL = func() {
+	Interceptor("tracing")
+	Service("ServiceWithClientInterceptor", func() {
+		Method("Method", func() {
+			ClientInterceptor("tracing")
+			Payload(String)
+			Result(String)
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+}
diff --git a/codegen/service/testing.go b/codegen/service/testing.go
new file mode 100644
index 00000000..28243ecf
--- /dev/null
+++ b/codegen/service/testing.go
@@ -0,0 +1,42 @@
+package service
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+// initDSL initializes the DSL environment and returns the root.
+func initDSL(t *testing.T) *expr.RootExpr {
+	// reset all roots and codegen data structures
+	Services = make(ServicesData)
+	eval.Reset()
+	expr.Root = new(expr.RootExpr)
+	expr.GeneratedResultTypes = new(expr.ResultTypesRoot)
+	expr.Root.API = expr.NewAPIExpr("test api", func() {})
+	expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()}
+	root := expr.Root
+	require.NoError(t, eval.Register(root))
+	require.NoError(t, eval.Register(expr.GeneratedResultTypes))
+	return root
+}
+
+// runDSL returns the DSL root resulting from running the given DSL.
+func runDSL(t *testing.T, dsl func()) *expr.RootExpr {
+	root := initDSL(t)
+	require.True(t, eval.Execute(dsl, nil))
+	require.NoError(t, eval.RunDSL())
+	return root
+}
+
+// runDSLWithError returns the DSL root and error from running the given DSL.
+func runDSLWithError(t *testing.T, dsl func()) (*expr.RootExpr, error) {
+	root := initDSL(t)
+	require.True(t, eval.Execute(dsl, nil))
+	err := eval.RunDSL()
+	require.Error(t, err)
+	return root, err
+}
diff --git a/dsl/interceptor_test.go b/dsl/interceptor_test.go
new file mode 100644
index 00000000..7e519768
--- /dev/null
+++ b/dsl/interceptor_test.go
@@ -0,0 +1,250 @@
+package dsl_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	. "goa.design/goa/v3/dsl"
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+func TestInterceptor(t *testing.T) {
+	cases := map[string]struct {
+		DSL    func()
+		Assert func(t *testing.T, intr *expr.InterceptorExpr)
+	}{
+		"valid-minimal": {
+			func() {
+				Interceptor("minimal", func() {})
+			},
+			func(t *testing.T, intr *expr.InterceptorExpr) {
+				require.NotNil(t, intr, "interceptor should not be nil")
+				assert.Equal(t, "minimal", intr.Name)
+			},
+		},
+		"valid-complete": {
+			func() {
+				Interceptor("complete", func() {
+					Description("test interceptor")
+					ReadPayload(func() {
+						Attribute("foo", String)
+					})
+					WritePayload(func() {
+						Attribute("bar", String)
+					})
+					ReadResult(func() {
+						Attribute("baz", String)
+					})
+					WriteResult(func() {
+						Attribute("qux", String)
+					})
+				})
+			},
+			func(t *testing.T, intr *expr.InterceptorExpr) {
+				require.NotNil(t, intr, "interceptor should not be nil")
+				assert.Equal(t, "test interceptor", intr.Description)
+
+				require.NotNil(t, intr.ReadPayload, "ReadPayload should not be nil")
+				rp := expr.AsObject(intr.ReadPayload.Type)
+				require.NotNil(t, rp, "ReadPayload should be an object")
+				assert.NotNil(t, rp.Attribute("foo"), "ReadPayload should have a foo attribute")
+
+				require.NotNil(t, intr.WritePayload, "WritePayload should not be nil")
+				wp := expr.AsObject(intr.WritePayload.Type)
+				require.NotNil(t, wp, "WritePayload should be an object")
+				assert.NotNil(t, wp.Attribute("bar"), "WritePayload should have a bar attribute")
+
+				require.NotNil(t, intr.ReadResult, "ReadResult should not be nil")
+				rr := expr.AsObject(intr.ReadResult.Type)
+				require.NotNil(t, rr, "ReadResult should be an object")
+				assert.NotNil(t, rr.Attribute("baz"), "ReadResult should have a baz attribute")
+
+				require.NotNil(t, intr.WriteResult, "WriteResult should not be nil")
+				wr := expr.AsObject(intr.WriteResult.Type)
+				require.NotNil(t, wr, "WriteResult should be an object")
+				assert.NotNil(t, wr.Attribute("qux"), "WriteResult should have a qux attribute")
+			},
+		},
+		"empty-name": {
+			func() {
+				Interceptor("", func() {})
+			},
+			func(t *testing.T, intr *expr.InterceptorExpr) {
+				assert.NotNil(t, eval.Context.Errors, "expected a validation error")
+			},
+		},
+		"duplicate-name": {
+			func() {
+				Interceptor("duplicate", func() {})
+				Interceptor("duplicate", func() {})
+			},
+			func(t *testing.T, intr *expr.InterceptorExpr) {
+				if eval.Context.Errors == nil {
+					t.Error("expected a validation error, got none")
+				}
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			eval.Context = &eval.DSLContext{}
+			expr.Root = new(expr.RootExpr)
+			tc.DSL()
+			if len(expr.Root.Interceptors) > 0 {
+				tc.Assert(t, expr.Root.Interceptors[0])
+			}
+		})
+	}
+}
+
+func TestServerInterceptor(t *testing.T) {
+	cases := map[string]struct {
+		DSL    func()
+		Assert func(t *testing.T, svc *expr.ServiceExpr, err error)
+	}{
+		"valid-reference": {
+			func() {
+				var testInterceptor = Interceptor("test", func() {})
+				Service("Service", func() {
+					ServerInterceptor(testInterceptor)
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.NoError(t, err)
+				require.NotNil(t, svc)
+				require.Len(t, svc.ServerInterceptors, 1, "should have 1 server interceptor")
+				assert.Equal(t, "test", svc.ServerInterceptors[0].Name)
+			},
+		},
+		"valid-by-name": {
+			func() {
+				Interceptor("test", func() {})
+				Service("Service", func() {
+					ServerInterceptor("test")
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.NoError(t, err)
+				require.NotNil(t, svc)
+				require.Len(t, svc.ServerInterceptors, 1, "should have 1 server interceptor")
+				assert.Equal(t, "test", svc.ServerInterceptors[0].Name)
+			},
+		},
+		"invalid-reference": {
+			func() {
+				Service("Service", func() {
+					ServerInterceptor(42) // Invalid type
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.Error(t, err)
+			},
+		},
+		"invalid-name": {
+			func() {
+				Service("Service", func() {
+					ServerInterceptor("invalid")
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.Error(t, err)
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			eval.Context = &eval.DSLContext{}
+			expr.Root = new(expr.RootExpr)
+			tc.DSL()
+			root, err := runDSL(t, tc.DSL)
+			tc.Assert(t, root.Services[0], err)
+		})
+	}
+}
+
+func TestClientInterceptor(t *testing.T) {
+	cases := map[string]struct {
+		DSL    func()
+		Assert func(t *testing.T, svc *expr.ServiceExpr, err error)
+	}{
+		"valid-reference": {
+			func() {
+				var testInterceptor = Interceptor("test", func() {})
+				Service("Service", func() {
+					ClientInterceptor(testInterceptor)
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.NoError(t, err)
+				require.NotNil(t, svc)
+				require.Len(t, svc.ClientInterceptors, 1, "should have 1 client interceptor")
+			},
+		},
+		"valid-by-name": {
+			func() {
+				Interceptor("test", func() {})
+				Service("Service", func() {
+					ClientInterceptor("test")
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.NoError(t, err)
+				require.NotNil(t, svc)
+				require.Len(t, svc.ClientInterceptors, 1, "should have 1 client interceptor")
+			},
+		},
+		"invalid-reference": {
+			func() {
+				Service("Service", func() {
+					ClientInterceptor(42) // Invalid type
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.Error(t, err)
+			},
+		},
+		"invalid-name": {
+			func() {
+				Service("Service", func() {
+					ClientInterceptor("invalid")
+				})
+			},
+			func(t *testing.T, svc *expr.ServiceExpr, err error) {
+				require.Error(t, err)
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			eval.Context = &eval.DSLContext{}
+			expr.Root = new(expr.RootExpr)
+			tc.DSL()
+			root, err := runDSL(t, tc.DSL)
+			tc.Assert(t, root.Services[0], err)
+		})
+	}
+}
+
+// runDSL returns the DSL root resulting from running the given DSL.
+func runDSL(t *testing.T, dsl func()) (*expr.RootExpr, error) {
+	t.Helper()
+	eval.Reset()
+	expr.Root = new(expr.RootExpr)
+	expr.GeneratedResultTypes = new(expr.ResultTypesRoot)
+	require.NoError(t, eval.Register(expr.Root))
+	require.NoError(t, eval.Register(expr.GeneratedResultTypes))
+	expr.Root.API = expr.NewAPIExpr("test api", func() {})
+	expr.Root.API.Servers = []*expr.ServerExpr{expr.Root.API.DefaultServer()}
+	if eval.Execute(dsl, nil) {
+		return expr.Root, eval.RunDSL()
+	} else {
+		return expr.Root, errors.New(eval.Context.Error())
+	}
+}
diff --git a/dsl/method_test.go b/dsl/method_test.go
index 30030fdb..d450ecf2 100644
--- a/dsl/method_test.go
+++ b/dsl/method_test.go
@@ -45,15 +45,14 @@ func TestMethod(t *testing.T) {
 					t.Fatalf("b: expected 1 method, got %d", len(methods))
 				}
 				method := methods[0]
-				doc := method.Docs
-				if doc == nil {
+				if method.Docs == nil {
 					t.Fatalf("b: method docs is nil")
 				}
-				if doc.Description != desc {
-					t.Errorf("b: expected docs description '%s' to match '%s' ", desc, doc.Description)
+				if method.Docs.Description != desc {
+					t.Errorf("b: expected docs description '%s' to match '%s' ", desc, method.Docs.Description)
 				}
-				if doc.URL != url {
-					t.Errorf("b: expected docs url '%s' to match '%s' ", url, doc.URL)
+				if method.Docs.URL != url {
+					t.Errorf("b: expected docs url '%s' to match '%s' ", url, method.Docs.URL)
 				}
 			},
 		},
@@ -74,19 +73,20 @@ func TestMethod(t *testing.T) {
 				method := methods[0]
 				if method == nil {
 					t.Fatalf("c: method is nil")
+					return // Make linter happy
 				}
-				payload := method.Payload
-				if payload == nil {
+				if method.Payload == nil {
 					t.Fatalf("c: method payload is nil")
+					return // Make linter happy
 				}
-				if payload.Description != desc {
-					t.Errorf("c: expected payload description '%s' to match '%s' ", desc, payload.Description)
+				if method.Payload.Description != desc {
+					t.Errorf("c: expected payload description '%s' to match '%s' ", desc, method.Payload.Description)
 				}
-				obj := expr.AsObject(payload.Type)
+				obj := expr.AsObject(method.Payload.Type)
 				if att := obj.Attribute("required"); att == nil {
 					t.Errorf("c: expected a payload field with key required")
 				}
-				if !payload.IsRequired("required") {
+				if !method.Payload.IsRequired("required") {
 					t.Errorf("c: expected the required field to be required")
 				}
 			},
diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go
index 60543808..b1d40f45 100644
--- a/expr/http_endpoint_test.go
+++ b/expr/http_endpoint_test.go
@@ -215,8 +215,8 @@ func TestHTTPEndpointParentRequired(t *testing.T) {
 		t.Fatal(`unexpected error, service "Child" not found`)
 	}
 	m := svc.Method("Method")
-	if m == nil {
-		t.Fatal(`unexpected error, method "Method" not found`)
+	if m == nil || m.Payload == nil {
+		t.Fatal(`unexpected error, method "Method" or its payload not found`)
 	}
 	if !m.Payload.IsRequired("ancestor_id") {
 		t.Errorf(`expected "ancestor_id" is required, but not so`)
diff --git a/expr/method_test.go b/expr/method_test.go
index bd2bef86..6d574596 100644
--- a/expr/method_test.go
+++ b/expr/method_test.go
@@ -154,3 +154,31 @@ func TestMethodExprIsPayloadStreaming(t *testing.T) {
 		}
 	}
 }
+
+func TestMethodExprValidateInterceptors(t *testing.T) {
+	cases := []struct {
+		Name  string
+		DSL   func()
+		Error string
+	}{
+		{"no-interceptors", testdata.NoInterceptorsDSL, ""},
+		{"valid-interceptors", testdata.ValidInterceptorsDSL, ""},
+		{"duplicate-interceptors", testdata.DuplicateInterceptorsDSL, ""}, // Duplicates are handled by merging
+		{"mixed-interceptors", testdata.MixedInterceptorsDSL, ""},
+		{"undefined-interceptor", testdata.UndefinedInterceptorDSL,
+			`ServerInterceptor: interceptor "undefined" not found in service "Service" method "Method"`},
+		{"empty-interceptor-name", testdata.EmptyInterceptorNameDSL,
+			`ServerInterceptor: interceptor name cannot be empty`},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			if c.Error == "" {
+				expr.RunDSL(t, c.DSL)
+				return
+			}
+			err := expr.RunInvalidDSL(t, c.DSL)
+			assert.Contains(t, err.Error(), c.Error)
+		})
+	}
+}
diff --git a/expr/testdata/interceptors_validate_dsls.go b/expr/testdata/interceptors_validate_dsls.go
new file mode 100644
index 00000000..32d1d99d
--- /dev/null
+++ b/expr/testdata/interceptors_validate_dsls.go
@@ -0,0 +1,83 @@
+package testdata
+
+import . "goa.design/goa/v3/dsl"
+
+var NoInterceptorsDSL = func() {
+	Service("Service", func() {
+		Method("Method", func() {
+			HTTP(func() {
+				GET("/")
+			})
+		})
+	})
+}
+
+var ValidInterceptorsDSL = func() {
+	Interceptor("api", func() {})
+	API("API", func() {
+		ServerInterceptor("api")
+	})
+
+	Service("Service", func() {
+		Interceptor("service", func() {})
+		ServerInterceptor("service")
+
+		Method("Method", func() {
+			Interceptor("method", func() {})
+			ServerInterceptor("method")
+		})
+	})
+}
+
+var DuplicateInterceptorsDSL = func() {
+	Interceptor("duplicate", func() {})
+	API("API", func() {
+		ServerInterceptor("duplicate")
+	})
+
+	Service("Service", func() {
+		ServerInterceptor("duplicate")
+		Method("Method", func() {
+			ServerInterceptor("duplicate")
+		})
+	})
+}
+
+var MixedInterceptorsDSL = func() {
+	Interceptor("api", func() {})
+	Interceptor("api-client", func() {})
+	API("API", func() {
+		ServerInterceptor("api")
+		ClientInterceptor("api-client")
+	})
+
+	Service("Service", func() {
+		Interceptor("service", func() {})
+		Interceptor("service-client", func() {})
+		ServerInterceptor("service")
+		ClientInterceptor("service-client")
+
+		Method("Method", func() {
+			Interceptor("method", func() {})
+			Interceptor("method-client", func() {})
+			ServerInterceptor("method")
+			ClientInterceptor("method-client")
+		})
+	})
+}
+
+var UndefinedInterceptorDSL = func() {
+	Service("Service", func() {
+		Method("Method", func() {
+			ServerInterceptor("undefined")
+		})
+	})
+}
+
+var EmptyInterceptorNameDSL = func() {
+	Service("Service", func() {
+		Method("Method", func() {
+			ServerInterceptor("")
+		})
+	})
+}
diff --git a/grpc/codegen/example_cli_test.go b/grpc/codegen/example_cli_test.go
index 9a9b59a7..aeb5fffe 100644
--- a/grpc/codegen/example_cli_test.go
+++ b/grpc/codegen/example_cli_test.go
@@ -2,6 +2,7 @@ package codegen
 
 import (
 	"bytes"
+	"path/filepath"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -9,6 +10,7 @@ import (
 	"goa.design/goa/v3/codegen/example"
 	ctestdata "goa.design/goa/v3/codegen/example/testdata"
 	"goa.design/goa/v3/expr"
+	"goa.design/goa/v3/grpc/codegen/testdata"
 )
 
 func TestExampleCLIFiles(t *testing.T) {
@@ -23,6 +25,7 @@ func TestExampleCLIFiles(t *testing.T) {
 		{"no-server-pkgpath", ctestdata.NoServerDSL, "my/pkg/path"},
 		{"server-hosting-service-subset-pkgpath", ctestdata.ServerHostingServiceSubsetDSL, "my/pkg/path"},
 		{"server-hosting-multiple-services-pkgpath", ctestdata.ServerHostingMultipleServicesDSL, "my/pkg/path"},
+		{"interceptors", testdata.InterceptorsDSL, ""},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
@@ -37,7 +40,8 @@ func TestExampleCLIFiles(t *testing.T) {
 				require.NoError(t, s.Write(&buf))
 			}
 			code := codegen.FormatTestCode(t, buf.String())
-			compareOrUpdateGolden(t, code, "client-"+c.Name+".golden")
+			golden := filepath.Join("testdata", "client-"+c.Name+".golden")
+			compareOrUpdateGolden(t, code, golden)
 		})
 	}
 }
diff --git a/grpc/codegen/parse_endpoint_test.go b/grpc/codegen/parse_endpoint_test.go
new file mode 100644
index 00000000..3a459e4b
--- /dev/null
+++ b/grpc/codegen/parse_endpoint_test.go
@@ -0,0 +1,40 @@
+package codegen
+
+import (
+	"bytes"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+	"goa.design/goa/v3/grpc/codegen/testdata"
+)
+
+func TestParseEndpointWithInterceptors(t *testing.T) {
+	cases := []struct {
+		Name string
+		DSL  func()
+	}{
+		{
+			Name: "endpoint-with-interceptors",
+			DSL:  testdata.InterceptorsDSL,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			RunGRPCDSL(t, c.DSL)
+			fs := ClientCLIFiles("", expr.Root)
+			require.Greater(t, len(fs), 1, "expected at least 2 files")
+			require.NotEmpty(t, fs[0].SectionTemplates)
+			var buf bytes.Buffer
+			for _, s := range fs[0].SectionTemplates {
+				require.NoError(t, s.Write(&buf))
+			}
+			code := codegen.FormatTestCode(t, buf.String())
+			golden := filepath.Join("testdata", "endpoint-"+c.Name+".golden")
+			compareOrUpdateGolden(t, code, golden)
+		})
+	}
+}
diff --git a/grpc/codegen/testdata/client-interceptors.golden b/grpc/codegen/testdata/client-interceptors.golden
new file mode 100644
index 00000000..44dadcce
--- /dev/null
+++ b/grpc/codegen/testdata/client-interceptors.golden
@@ -0,0 +1,30 @@
+import (
+	"fmt"
+	cli "grpc/cli/test"
+	"os"
+
+	"./interceptors"
+	goa "goa.design/goa/v3/pkg"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) {
+	conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err)
+	}
+	serviceWithInterceptorsInterceptors := interceptors.NewServiceWithInterceptorsClientInterceptors()
+	return cli.ParseEndpoint(
+		conn,
+		serviceWithInterceptorsInterceptors,
+	)
+}
+
+func grpcUsageCommands() string {
+	return cli.UsageCommands()
+}
+
+func grpcUsageExamples() string {
+	return cli.UsageExamples()
+}
diff --git a/grpc/codegen/testdata/dsls.go b/grpc/codegen/testdata/dsls.go
index f7c7c5c7..8bdd24b8 100644
--- a/grpc/codegen/testdata/dsls.go
+++ b/grpc/codegen/testdata/dsls.go
@@ -1009,3 +1009,34 @@ var CustomMessageNameDSL = func() {
 		})
 	})
 }
+
+var InterceptorsDSL = func() {
+	var LogInterceptor = Interceptor("Log", func() {
+		Description("Logs request and response details")
+	})
+	var MetricsInterceptor = Interceptor("Metrics", func() {
+		Description("Collects metrics for the operation")
+	})
+
+	API("TestAPI", func() {
+		Title("Test API")
+		Server("Test", func() {
+			Description("Test server")
+		})
+	})
+
+	Service("ServiceWithInterceptors", func() {
+		ClientInterceptor(LogInterceptor)
+		Method("MethodA", func() {
+			ClientInterceptor(MetricsInterceptor)
+			Payload(String)
+			Result(String)
+			GRPC(func() {})
+		})
+		Method("MethodB", func() {
+			Payload(Int)
+			Result(Int)
+			GRPC(func() {})
+		})
+	})
+}
diff --git a/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden b/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden
new file mode 100644
index 00000000..4899bed5
--- /dev/null
+++ b/grpc/codegen/testdata/endpoint-endpoint-with-interceptors.golden
@@ -0,0 +1,178 @@
+// Test gRPC client CLI support package
+//
+// Command:
+// goa
+
+package cli
+
+import (
+	servicewithinterceptors "/service_with_interceptors"
+	"flag"
+	"fmt"
+	servicewithinterceptorsc "grpc/service_with_interceptors/client"
+	"os"
+
+	goa "goa.design/goa/v3/pkg"
+	grpc "google.golang.org/grpc"
+)
+
+// UsageCommands returns the set of commands and sub-commands using the format
+//
+//	command (subcommand1|subcommand2|...)
+func UsageCommands() string {
+	return `service-with-interceptors (method-a|method-b)
+`
+}
+
+// UsageExamples produces an example of a valid invocation of the CLI tool.
+func UsageExamples() string {
+	return os.Args[0] + ` service-with-interceptors method-a --message '{
+      "field": "Sapiente est."
+   }'` + "\n" +
+		""
+}
+
+// ParseEndpoint returns the endpoint and payload as specified on the command
+// line.
+func ParseEndpoint(
+	cc *grpc.ClientConn,
+	inter servicewithinterceptors.ClientInterceptors,
+	opts ...grpc.CallOption,
+) (goa.Endpoint, any, error) {
+	var (
+		serviceWithInterceptorsFlags = flag.NewFlagSet("service-with-interceptors", flag.ContinueOnError)
+
+		serviceWithInterceptorsMethodAFlags       = flag.NewFlagSet("method-a", flag.ExitOnError)
+		serviceWithInterceptorsMethodAMessageFlag = serviceWithInterceptorsMethodAFlags.String("message", "", "")
+
+		serviceWithInterceptorsMethodBFlags       = flag.NewFlagSet("method-b", flag.ExitOnError)
+		serviceWithInterceptorsMethodBMessageFlag = serviceWithInterceptorsMethodBFlags.String("message", "", "")
+	)
+	serviceWithInterceptorsFlags.Usage = serviceWithInterceptorsUsage
+	serviceWithInterceptorsMethodAFlags.Usage = serviceWithInterceptorsMethodAUsage
+	serviceWithInterceptorsMethodBFlags.Usage = serviceWithInterceptorsMethodBUsage
+
+	if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
+		return nil, nil, err
+	}
+
+	if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND)
+		return nil, nil, fmt.Errorf("not enough arguments")
+	}
+
+	var (
+		svcn string
+		svcf *flag.FlagSet
+	)
+	{
+		svcn = flag.Arg(0)
+		switch svcn {
+		case "service-with-interceptors":
+			svcf = serviceWithInterceptorsFlags
+		default:
+			return nil, nil, fmt.Errorf("unknown service %q", svcn)
+		}
+	}
+	if err := svcf.Parse(flag.Args()[1:]); err != nil {
+		return nil, nil, err
+	}
+
+	var (
+		epn string
+		epf *flag.FlagSet
+	)
+	{
+		epn = svcf.Arg(0)
+		switch svcn {
+		case "service-with-interceptors":
+			switch epn {
+			case "method-a":
+				epf = serviceWithInterceptorsMethodAFlags
+
+			case "method-b":
+				epf = serviceWithInterceptorsMethodBFlags
+
+			}
+
+		}
+	}
+	if epf == nil {
+		return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn)
+	}
+
+	// Parse endpoint flags if any
+	if svcf.NArg() > 1 {
+		if err := epf.Parse(svcf.Args()[1:]); err != nil {
+			return nil, nil, err
+		}
+	}
+
+	var (
+		data     any
+		endpoint goa.Endpoint
+		err      error
+	)
+	{
+		switch svcn {
+		case "service-with-interceptors":
+			c := servicewithinterceptorsc.NewClient(cc, opts...)
+			switch epn {
+
+			case "method-a":
+				endpoint = c.MethodA()
+				endpoint = servicewithinterceptors.WrapMethodAClientEndpoint(endpoint, inter)
+				data, err = servicewithinterceptorsc.BuildMethodAPayload(*serviceWithInterceptorsMethodAMessageFlag)
+			case "method-b":
+				endpoint = c.MethodB()
+				endpoint = servicewithinterceptors.WrapMethodBClientEndpoint(endpoint, inter)
+				data, err = servicewithinterceptorsc.BuildMethodBPayload(*serviceWithInterceptorsMethodBMessageFlag)
+			}
+		}
+	}
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return endpoint, data, nil
+}
+
+// serviceWithInterceptorsUsage displays the usage of the
+// service-with-interceptors command and its subcommands.
+func serviceWithInterceptorsUsage() {
+	fmt.Fprintf(os.Stderr, `Service is the ServiceWithInterceptors service interface.
+Usage:
+    %[1]s [globalflags] service-with-interceptors COMMAND [flags]
+
+COMMAND:
+    method-a: MethodA implements MethodA.
+    method-b: MethodB implements MethodB.
+
+Additional help:
+    %[1]s service-with-interceptors COMMAND --help
+`, os.Args[0])
+}
+func serviceWithInterceptorsMethodAUsage() {
+	fmt.Fprintf(os.Stderr, `%[1]s [flags] service-with-interceptors method-a -message JSON
+
+MethodA implements MethodA.
+    -message JSON: 
+
+Example:
+    %[1]s service-with-interceptors method-a --message '{
+      "field": "Sapiente est."
+   }'
+`, os.Args[0])
+}
+
+func serviceWithInterceptorsMethodBUsage() {
+	fmt.Fprintf(os.Stderr, `%[1]s [flags] service-with-interceptors method-b -message JSON
+
+MethodB implements MethodB.
+    -message JSON: 
+
+Example:
+    %[1]s service-with-interceptors method-b --message '{
+      "field": 3141052981090487272
+   }'
+`, os.Args[0])
+}