Getting Started with NestGo: A NestJS-Style Web Framework for Go

This NestGo tutorial walks you through building your first application with NestGo, the NestJS-for-Go web framework. In a few minutes you will install the core module, choose an HTTP adapter, and run a working REST API in Golang.

Prerequisites

Requirement Version Notes
Go 1.25.14 or later Older 1.25.x toolchains are upgraded automatically via the go directive. The patch level matters — it includes Go standard-library security fixes.
An HTTP adapter Gin or Fiber Separate modules; you install exactly one.

Check your Go version:

go version

How to install NestGo

Create a new module and install the NestGo core:

mkdir myapp && cd myapp
go mod init myapp
go get github.com/ashrafAli23/nestgo

Choose one adapter

NestGo’s core is interfaces only — an adapter plugs in the actual HTTP engine. Install one of the following (they are separate Go modules):

# Option A: Gin
go get github.com/ashrafAli23/nestgo-gin-adapter
# Option B: Fiber
go get github.com/ashrafAli23/nestgo-fiber-adapter

You can swap adapters later by changing a single line in main.go — handlers, guards, interceptors, pipes, and middleware work identically on both.

Quick start: your first NestGo app

Create main.go with a config and a controller. This is a complete, runnable Go web application — no DI container required:

package main

import (
    "context"
    "log"

    "github.com/ashrafAli23/nestgo/app"
    "github.com/ashrafAli23/nestgo/core"
    "github.com/ashrafAli23/nestgo/middleware"
    ginadapter "github.com/ashrafAli23/nestgo-gin-adapter"
)

// ─── DTO ────────────────────────────────────────────────────────────────────

type CreateUserDTO struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (d *CreateUserDTO) Validate() error {
    if d.Name == "" {
        return core.ErrBadRequest("name is required")
    }
    return nil
}

// ─── Controller ─────────────────────────────────────────────────────────────

type UserController struct{}

func NewUserController() *UserController { return &UserController{} }

func (c *UserController) Prefix() string { return "/users" }

func (c *UserController) RegisterRoutes(r core.Router) {
    r.GET("/", core.Handle1(core.Q("search", ""), c.List))
    r.GET("/:id", core.Handle2(core.RCtx(), core.PInt64("id"), c.GetByID))
    r.POST("/", core.HandleC1(core.B[CreateUserDTO](), c.Create))
}

func (c *UserController) List(search string) (any, error) {
    users := []map[string]any{
        {"name": "John"},
        {"name": "Jane"},
    }
    return users, nil
}

func (c *UserController) GetByID(ctx context.Context, id int64) (any, error) {
    // ctx carries request cancellation — pass it to DB, gRPC, HTTP calls
    return map[string]any{"id": id, "name": "John"}, nil
}

func (c *UserController) Create(ctx core.Context, dto *CreateUserDTO) error {
    return ctx.JSON(201, map[string]any{"name": dto.Name, "email": dto.Email})
}

// ─── Main ───────────────────────────────────────────────────────────────────

func main() {
    cfg := core.DefaultConfig()
    cfg.Addr = ":3000"
    cfg.GlobalPrefix = "/api"

    a := app.New(cfg, ginadapter.New)
    a.Use(middleware.Recovery(), middleware.Logger(), middleware.RequestID())
    a.Register(NewUserController())
    if err := a.Run(); err != nil {
        log.Fatal(err)
    }
}

Using Fiber instead? Change one line:

import fiberadapter "github.com/ashrafAli23/nestgo-fiber-adapter"

a := app.New(cfg, fiberadapter.New)

With dependency injection

Prefer a DI container? Swap the main above for this one — same DTO, same controller, everything else on this page stays identical:

func main() {
    config := core.DefaultConfig()
    config.Addr = ":3000"
    config.GlobalPrefix = "/api"

    app := di.NewApp(config, ginadapter.New,
        di.Invoke(func(server core.Server) {
            server.Use(middleware.Recovery(), middleware.Logger(), middleware.RequestID())
        }),
        di.Module("users", di.Controller(NewUserController)),
    )
    app.Run()
}

This variant imports "github.com/ashrafAli23/nestgo/di" instead of app; no fx import needed.

Which should I use?

Both bootstraps behave identically — same controllers, same middleware ordering, same lifecycle hooks, same shutdown sequence. Reach for app for a small service, or when you’d rather not learn a DI container. Reach for di once you have many modules with deep constructor graphs, or want NestJS-style modules and providers. See Application Without DI and Dependency Injection for the full comparison.

What each part does

Config

core.DefaultConfig() returns sensible defaults: Addr: ":8080", a 4 MB BodyLimit, 10-second read/write timeouts, and the default error handler. The example overrides two fields:

Field Effect
Addr The listen address — :3000 here.
GlobalPrefix Prepended to every route, so the controller’s /users routes become /api/users.

Config also carries global guards, pipes, interceptors, filters, TLS files, health checks, and graceful-shutdown hooks — see the Production Guide for the full core.Config field reference.

Controller

A controller is any struct implementing RegisterRoutes(router core.Router). Two optional interfaces add behavior automatically:

  • Prefix() string — NestGo detects PrefixedController and groups all routes under the prefix (/users here).
  • Version() string — VersionedController opts the controller into API versioning.

Inside RegisterRoutes, the handle builders (Handle1, Handle2, HandleC1, …) combine type-safe extractors with plain Go functions:

  • core.Q("search", "") — query parameter with a default (NestJS @Query()).
  • core.PInt64("id") — path parameter parsed as int64, ideal for database IDs.
  • core.RCtx() — the request’s context.Context, so services see cancellation and timeouts.
  • core.B[CreateUserDTO]() — parses the JSON body into the DTO and calls its Validate() method (NestJS @Body() + ValidationPipe).

Handle1/Handle2 auto-respond with JSON 200 from your function’s return value. HandleC1 passes core.Context first, giving you full response control — the example uses it to return 201 Created.

di.NewApp and app.Run()

di.NewApp(config, provider, opts...) builds an uber/fx application, using the di wrappers so nothing in your code has to import go.uber.org/fx directly:

  1. Provides the *core.Config and creates the core.Server from your adapter’s constructor (ginadapter.New or fiberadapter.New).
  2. Derives the root core.Router, applying GlobalPrefix, global middleware, and global guards/pipes/interceptors/filters from the config.
  3. Applies your di.Options — middleware setup via di.Invoke, feature modules via di.Module. These run before routes are registered, which matters on both adapters: middleware chains are composed when each route is registered, so Use() after registration does not affect that route. Raw fx.Options (fx.Provide, fx.Invoke, fx.Annotate, …) still work anywhere a di.Option is expected — di.Option is just a type alias for fx.Option.
  4. Appends di.CoreModule, which registers health endpoints, wires every di.Controller(...) constructor into the router, hooks the server into the fx lifecycle, and registers OnModuleInit/OnModuleDestroy hooks.

app.Run() starts the server and blocks. SIGINT/SIGTERM triggers graceful shutdown: in-flight requests drain for up to config.ShutdownTimeout (default 10 seconds), then your config.OnShutdown hooks run. If the server fails to start (port in use, bad certificate), the app shuts down with exit code 1 instead of hanging without a listener.

di.Controller(NewUserController) registers the constructor so the container collects it into the controller group — the NestGo equivalent of listing a controller in a NestJS @Module(). See Dependency Injection.

Run and test the API

Start the app:

go run .

Then exercise the three routes with curl:

# List users (query extractor with default)
curl http://localhost:3000/api/users
# → [{"name":"John"},{"name":"Jane"}]

# Get a user by int64 ID
curl http://localhost:3000/api/users/42
# → {"id":42,"name":"John"}

# Create a user (201 Created)
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"ada@example.com"}'
# → {"email":"ada@example.com","name":"Ada"}

# Validation failure — Validate() returns core.ErrBadRequest
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"email":"no-name@example.com"}'
# → 400 Bad Request: name is required

Project layout for a real Go application

For anything beyond a demo, organize your Golang project by feature, with one fx module per feature — the same mental model as NestJS modules:

myapp/
├── go.mod
├── main.go                  # config + adapter + module list
└── internal/
    ├── users/
    │   ├── module.go        # var Module = di.Module("users", ...)
    │   ├── controller.go    # routes + handlers
    │   ├── service.go       # business logic
    │   └── dto.go           # request/response DTOs
    └── orders/
        ├── module.go
        ├── controller.go
        └── service.go

Each feature exports a single fx.Option:

// internal/users/module.go
package users

import (
    "github.com/ashrafAli23/nestgo/di"
)

var Module = di.Module("users",
    di.Service(NewUserService),                 // injected into the controller
    di.Controller(NewUserController),
)

The controller receives its dependencies through its constructor:

// internal/users/controller.go
type UserController struct {
    service *UserService
}

func NewUserController(service *UserService) *UserController {
    return &UserController{service: service}
}

And main.go stays a thin composition root:

func main() {
    config := core.DefaultConfig()
    config.Addr = ":3000"
    config.GlobalPrefix = "/api"
    config.HealthCheck = true // GET /health for K8s liveness probes

    app := di.NewApp(config, ginadapter.New,
        users.Module,
        orders.Module,
    )
    app.Run()
}

Adding a feature means adding a directory and one line in main.go — controllers register their own routes, and the DI container wires everything together. Browse the full source and more examples at github.com/ashrafAli23/nestgo.

Next steps