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/module schematics. 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/swagger generates 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 via c.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 UserService becomes type UserService struct{...} with func 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(), return core.ErrNotFound("user not found"). Exception filters still catch and format them — see Exception Filters.
  • DTO validation moves to struct tags. class-validator’s @IsEmail() becomes validate:"required,email" tags checked via core.SetValidateFunc, with an optional Validate() error method for business rules — see Validation.
  • async/await disappears. Go handlers are synchronous-looking code on cheap goroutines. Pass context.Context (via the core.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