NestGo vs NestJS: A NestJS Alternative for Go
NestGo brings the architecture NestJS developers know — controllers, guards, interceptors, pipes, exception filters, and dependency injection — to Go (Golang), without decorators or runtime reflection. This page maps every NestJS concept to its NestGo equivalent and shows the same endpoint written in both frameworks.
Is there a NestJS for Go?
There is no official NestJS port for Go, and a literal port would not be idiomatic: TypeScript decorators and reflection-based metadata do not exist in Go. NestGo is instead a NestJS-inspired framework built the Go way — the same layered architecture and request lifecycle (Filters → Guards → Interceptors → Handler), expressed through interfaces, generics, and explicit registration. Your knowledge of NestJS transfers almost one-to-one; only the syntax changes.
NestJS to NestGo concept mapping
| NestJS | NestGo equivalent | Notes |
|---|---|---|
@Controller('users') | Controller interface + Prefix() string | Implement RegisterRoutes(r core.Router); optional PrefixedController auto-prefixes routes |
@Get(':id'), @Post() | r.GET("/:id", ...), r.POST("/", ...) | Explicit registration inside RegisterRoutes |
@Body() dto | core.B[CreateUserDTO]() | Generic extractor — parses and validates the body into *CreateUserDTO |
@Param('id') | core.P("id") | Path parameter as string |
@Param('id', ParseIntPipe) | core.PInt("id") / core.PInt64("id") | Typed path parameters; PInt64 for database IDs |
@Query('search') | core.Q("search", "default") | Optional default value |
@Query() dto | core.QDto[FilterDTO]() | Query string into a struct |
@Headers('key') | core.H("key", required) | Header extractor |
@Injectable() + constructor injection | fx.Provide(NewService) | Constructor injection via uber/fx — no decorators needed |
@Module({...}) | fx.Module("users", fx.Provide(...)) | Modules group providers and controllers |
CanActivate / @UseGuards() | core.Guard / core.GuardFunc + core.UseGuards() | Global, per-group, or per-route |
NestInterceptor / @UseInterceptors() | core.Interceptor / core.InterceptorFunc + core.UseInterceptors() | Wraps handler execution before/after |
PipeTransform | core.Pipe + core.WithPipes() | Runs after extraction, before the handler |
ExceptionFilter / @UseFilters() | core.ExceptionFilter + core.UseFilters() | CanHandle(err) + Handle(c, err) |
@SetMetadata() + Reflector | core.WithMeta(key, value) + c.Get(key) | Route metadata readable in guards/interceptors |
ValidationPipe + class-validator | core.SetValidateFunc(validator.Validate) | Struct tags via nestgo-validator; or implement Validate() error on the DTO |
HttpException | core.HTTPError (core.ErrBadRequest, core.ErrNotFound, …) | Errors are returned, not thrown |
NestFactory.create() in main.ts | di.NewApp(config, ginadapter.New, ...) + app.Run() | Adapter chosen as a constructor argument |
app.setGlobalPrefix('api') | config.GlobalPrefix = "/api" | Config field |
@Version('2') | VersionedController with Version() string | URI, header, or media-type strategies via config.Versioning |
OnModuleInit / OnModuleDestroy | OnModuleInit(ctx) / OnModuleDestroy(ctx) | Registered with di.AsInitHook / di.AsDestroyHook |
| Platform adapters (Express / Fastify) | nestgo-gin-adapter / nestgo-fiber-adapter | Swap engines by changing one line |
@Sse() | core.SSE(c, stream) + core.NewSSEStream(n) | Channel-based Server-Sent Events |
The same endpoint in NestJS (TypeScript) and NestGo (Go)
A users controller with a typed path parameter, a validated body, and a custom status code.
NestJS:
@Controller('users')
export class UserController {
constructor(private readonly service: UserService) {}
@Get(':id')
getById(@Param('id', ParseIntPipe) id: number) {
return this.service.findById(id);
}
@Post()
@HttpCode(201)
create(@Body() dto: CreateUserDto) {
return this.service.create(dto);
}
}
NestGo:
type UserController struct {
service *UserService
}
func NewUserController(service *UserService) *UserController {
return &UserController{service: service}
}
func (ctrl *UserController) Prefix() string { return "/users" }
func (ctrl *UserController) RegisterRoutes(r core.Router) {
r.GET("/:id", core.Handle1(core.PInt64("id"), ctrl.GetByID))
r.POST("/", core.HandleC1(core.B[CreateUserDTO](), ctrl.Create))
}
func (ctrl *UserController) GetByID(id int64) (any, error) {
return ctrl.service.FindByID(id)
}
func (ctrl *UserController) Create(c core.Context, dto *CreateUserDTO) error {
user, err := ctrl.service.Create(dto)
if err != nil {
return err
}
return c.JSON(201, user)
}
Wiring it up — the NestGo equivalent of a NestJS module plus main.ts:
func main() {
config := core.DefaultConfig()
config.Addr = ":3000"
config.GlobalPrefix = "/api"
app := di.NewApp(config, ginadapter.New,
fx.Module("users",
fx.Provide(NewUserService),
fx.Provide(di.AsController(NewUserController)),
),
)
app.Run()
}
The shape is identical: extractors play the role of parameter decorators, RegisterRoutes plays the role of method decorators, and fx.Module plays the role of @Module. Handle1 responds with JSON 200 automatically (or 204 when the result is nil); HandleC1 hands you the Context when you need a custom status such as 201.
What NestGo deliberately does differently
Generics instead of decorators. Go has no decorators, so NestGo uses generic extractors (B[T](), PInt64("id"), QDto[T]()) with Handle1–Handle4 builders. The result is checked by the compiler: pass the wrong handler signature for your extractors and the build fails. In NestJS, a mistyped @Param surfaces at runtime.
No reflection at request time. NestJS scans decorator metadata through reflect-metadata. NestGo resolves everything into plain function calls at startup; the hot path is closures and interface calls. Dependency injection (uber/fx) also runs once at startup — a missing provider fails the app boot, not a request.
Single static binary. go build produces one self-contained executable — no node_modules, no runtime to install, small containers, fast cold starts.
Zero-dependency core. The core package is interfaces only. Your handlers never import Gin or Fiber; swapping engines is a one-line change in main.go. NestJS has a similar Express/Fastify split, but NestGo enforces it at the package-dependency level.
Explicit over magical. Routes are registered in code you can read and step through, not discovered by scanning metadata. That is a Go-philosophy choice: slightly more typing, much less magic.
What NestJS has that NestGo doesn’t
Being honest matters more than winning a comparison:
- CLI generator. NestJS ships
nest g controller/service/moduleschematics. NestGo has no code generator — you write the (small) boilerplate yourself. - Built-in transports. NestJS bundles GraphQL, and microservice transports for Kafka, NATS, gRPC, RabbitMQ, and MQTT. NestGo is HTTP-focused (REST, SSE); for gRPC or Kafka you use the standard Go libraries alongside it, sharing services through the same DI container.
- Decorator-driven OpenAPI.
@nestjs/swaggergenerates API docs from decorators. NestGo has no equivalent generator today. - First-class WebSocket gateways. NestGo detects upgrades (
IsWebSocketRequest) but delegates the WebSocket handling to the adapter viac.Underlying(). - Ecosystem size. NestJS has years of official
@nestjs/*packages and community modules. NestGo covers the common production middleware (CORS, Helmet, rate limiting, CSRF, ETag, idempotency, tracing, upload) in its own middleware package, but the surrounding ecosystem is Go’s general ecosystem, not NestGo-specific plugins.
If your workload is REST APIs and backend services, NestGo covers the NestJS features you actually use daily. If you need GraphQL federation or decorator-generated Swagger out of the box, NestJS still has the edge.
Migration tips for NestJS developers learning Go
- Classes become structs + constructor functions.
@Injectable() class UserServicebecomestype UserService struct{...}withfunc NewUserService(...) *UserService. Constructor injection works the same way — fx reads the constructor’s parameter types. - Errors are values, not exceptions. Instead of
throw new NotFoundException(), returncore.ErrNotFound("user not found"). Exception filters still catch and format them — see Exception Filters. - DTO validation moves to struct tags. class-validator’s
@IsEmail()becomesvalidate:"required,email"tags checked viacore.SetValidateFunc, with an optionalValidate() errormethod for business rules — see Validation. async/awaitdisappears. Go handlers are synchronous-looking code on cheap goroutines. Passcontext.Context(via thecore.RCtx()extractor) into database and HTTP calls so cancellation propagates.- Keep the same project layout. Module-per-feature (
fx.Module("users", ...)) maps directly to your NestJS module folders, so the mental model of the codebase survives the migration. - Start from the quick start. The Getting Started guide builds the same CRUD controller you would scaffold with the Nest CLI.
Next steps
- Getting Started — build your first NestGo app in Go
- Extractors — the full
@Body/@Param/@Queryequivalent reference - Dependency Injection — modules and providers with uber/fx