DEV Community

Cover image for From Node.js to Go: How to Smoothly Transition from NestJS and Fall in Love with the Sponge Framework
zhuyasen
zhuyasen

Posted on

From Node.js to Go: How to Smoothly Transition from NestJS and Fall in Love with the Sponge Framework

Introduction

Hey NestJS veterans, let's be honest, developing APIs with decorators feels like waltzing on your keyboard—a spin with @Controller, a tiptoe with @Get, an elegant bow with @Injectable, and voilà, a decent backend service is up and running in no time. TypeScript's type checking is like a considerate butler, and the CLI's nest g is practically a magic wand in the coding world. A few keystrokes, and your project scaffold magically appears—it's exhilarating!

However, when the requirements suddenly shift to handling real-time data from millions of smart light bulbs, or your boss slams the table shouting, "Cut the server budget in half!", do you find yourself sighing at Node.js's single thread: "Buddy, can your small frame handle this?" Just then, Go strides in confidently, showcasing its concurrency primitives, compilation speed, and memory efficiency... Wow, who wouldn't be tempted?

But, when you eagerly open your Go IDE, ready to make your mark—wait, where are my @Decorators? Where's my dependency injection? My code generator? Switching from NestJS's "fully automatic coffee machine" to Go's "manual coffee bean grinding" mode, the drop in experience is like going from a five-star hotel buffet back to instant noodles with a sausage...

Five "Culture Shocks" for Node.js Developers Switching to Go

1. CLI Dependency Withdrawal

In NestJS:

# Just a few keystrokes, and the project scaffold magically appears!
nest new my-project
nest g module users
nest g controller users
nest g service users
Enter fullscreen mode Exit fullscreen mode

In native Go:

mkdir my-project
cd my-project
go mod init my-project
mkdir -p users/handlers users/services
touch users/handlers/users.go users/services/users.go
# Then start manually writing a bunch of boilerplate code...
Enter fullscreen mode Exit fullscreen mode

2. The Loss of Modularity and DI

The elegance of NestJS:

// Watch me wave my decorator wand, dependencies auto-injected!
@Module({
  imports: [DatabaseModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

The "simplicity" of native Go:

// You need to manually initialize all dependencies
db := NewDB()
userRepo := NewUserRepository(db)
userService := NewUserService(userRepo)
userHandler := NewUserHandler(userService)
Enter fullscreen mode Exit fullscreen mode

3. Missing Type Systems and Decorators

The comfort zone of NestJS:

// Route decorators are more versatile than a magician's hat
@Get(':id')
async getUser(@Param('id') id: string): Promise<UserDto> {
  return this.userService.findOne(id);
}
Enter fullscreen mode Exit fullscreen mode

The explicit style of native Go:

func GetUser(c *gin.Context) {
    id := c.Param("id")
    user, err := userService.FindOne(id)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, user)
}
Enter fullscreen mode Exit fullscreen mode

4. Shift in Asynchronous Programming Mindset

Node.js's linear thinking:

// `await` is my time-stopper; code must follow my script
async function getUserWithOrders(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(userId);
  return { ...user, orders };
}
Enter fullscreen mode Exit fullscreen mode

Go's concurrency philosophy:

// Welcome to the concurrency circus! Watch me juggle multiple task balls simultaneously!
func GetUserWithOrders(userId string) (UserWithOrders, error) {
    var user User
    var orders []Order
    var userErr, ordersErr error

    wg := sync.WaitGroup{}
    wg.Add(2)

    go func() {
        defer wg.Done()
        user, userErr = GetUser(userId)
    }()

    go func() {
        defer wg.Done()
        orders, ordersErr = GetOrders(userId)
    }()

    wg.Wait()

    if userErr != nil || ordersErr != nil {
        return UserWithOrders{}, fmt.Errorf("errors: %v, %v", userErr, ordersErr)
    }

    return UserWithOrders{User: user, Orders: orders}, nil
}
Enter fullscreen mode Exit fullscreen mode

5. Change in Error Handling Habits

Node.js's try-catch:

// Error? Just toss it into the black hole for now
try {
  const result = await someAsyncOperation();
} catch (err) {
  console.error('Oops:', err);
}
Enter fullscreen mode Exit fullscreen mode

Go's explicit error handling:

// Every error is a traffic light; must stop and check!
result, err := SomeOperation()
if err != nil {
    log.Printf("Oops: %v", err)
    return err
}
Enter fullscreen mode Exit fullscreen mode

Sponge Framework – The Savior for Node.js to Go Transition Arrives

1. What is Sponge?

sponge is a powerful and easy-to-use Go development framework. Its core philosophy is to reverse-generate modular code by parsing SQL, Protobuf, and JSON files. These code modules can be flexibly combined into various types of complete backend services.

sponge provides a one-stop project development solution with excellent project engineering capabilities, covering code generation, development, testing, API documentation, and deployment. It helps developers easily build stable and reliable high-performance backend service systems (including RESTful API, gRPC, HTTP+gRPC, gRPC Gateway, etc.) in a "low-code" manner.

2. Sponge's Three Killer Features

① Code Generator – Faster Than Copy-Pasting

# Start the code generation UI
sponge run
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:24631, and you'll see:

sponge-ui

  • Automatically generate RESTful API, gRPC, HTTP+gRPC, gRPC Gateway, etc., service code
  • One-click CRUD code generation
  • Custom APIs only require filling in business logic

🚀 Effect: What used to take a day's work, now done in 1 minute!

② Modular Design – Project Structure as Clear as LEGOs

// Auto-generated user module structure
user/
├── api/
└── internal/
    ├── service/    // Business logic layer
    ├── dao/        // Data access layer
    ├── model/      // Entity classes
    ├── server/     // Service
    └── cache/      // Cache
Enter fullscreen mode Exit fullscreen mode

③ Built-in Enterprise-Grade Components – Say Goodbye to Decision Paralysis

Web Framework √
gRPC Framework √
ORM √
Configuration Management √
Logging √ 
Tracing √
Monitoring √
Service Discovery √
...
Enter fullscreen mode Exit fullscreen mode

Practical Comparison: Creating a User API

In NestJS:

// user.controller.ts
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string): Promise<User> {
    return this.userService.findOne(id);
  }
}

// user.service.ts
@Injectable()
export class UserService {
  constructor(private userRepo: UserRepository) {}

  async findOne(id: string): Promise<User> {
    return this.userRepo.findOne(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Sponge-generated version:

// Auto-generated handler
func (h *userHandler) CreateUser(c *gin.Context) { // Note: Original had CreatUser, common typo, corrected to CreateUser
    var req pb.CreateUserRequest // Corrected typo from CreatUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        response.Error(c, err)
        return
    }

    // Assuming h.dao.CreateUser exists and follows this pattern.
    // The original snippet was:
    // if result, err := h.dao.CreatUser(ctx, &req); err != nil {
    //     return 0, err // This implies the DAO returns (uint64, error)
    // }
    // For a create operation, it might not return a result directly in the handler like this.
    // More typically, you'd call the service/DAO and then respond.
    // Let's adjust to a more common pattern for a handler.
    _, err := h.service.Create(c.Request.Context(), &req) // Example: calling a service method
    if err != nil {
        response.Error(c, err) // Assuming response.Error handles logging and JSON response
        return
    }

    response.Success(c)
}

// Auto-generated DAO (example, assuming direct call from handler was intended to be to DAO)
// The original was:
// func (d *dao) CreatUser(ctx context.Context, req *pb.CreatUserRequest) (uint64, error) {
//     // business logic code ...
//     if err := CreateUser(ctx, req); err != nil { // This CreateUser is undefined in this context
//         return 0, err
//     }
// }
// Let's make it more concrete based on typical DAO patterns
func (d *userDao) Create(ctx context.Context, req *pb.CreateUserRequest) (uint64, error) {
    // Business logic... e.g., convert req to a model, save to DB
    // userModel := &model.User{Name: req.Name, Email: req.Email} // Example
    // result, err := d.db.Create(userModel).Error // Example with GORM
    // if err != nil {
    //     return 0, err
    // }
    // return userModel.ID, nil
    // For simplicity, let's assume the business logic is here:
    log.Printf("Creating user: %v", req.GetName()) // Example of using the request
    // Placeholder for actual database interaction
    var userID uint64 = 1 // Dummy ID
    return userID, nil 
}


// Apart from the business logic, all other code is generated by sponge.
Enter fullscreen mode Exit fullscreen mode

🔍 See the difference? Although the syntax is different, the development experience is highly consistent!

Why Node.js Developers Should Try Sponge?

  • Seamless Productivity Transition: Sponge's CLI and code generation capabilities let you experience NestJS-like development efficiency in the Go world.

  • Familiar "Recipe": Modular design, API definition methods, and microservice component integration will make you feel the familiarity of using NestJS.

  • "Best of Both Worlds": Enjoy the performance and concurrency advantages of Go while getting a NestJS-like efficient development experience.

  • Microservices "Power Tool": Sponge is inherently designed for microservices, integrating essential components like service governance, tracing, and monitoring.

Conclusion

The hardest part of transitioning to Go isn't the syntax, but the shift in mindset. While you can't write Node.js-style code in Go, you can enjoy a NestJS-like development experience with Sponge!

What are you waiting for? Get moving! Follow the official documentation and whip up a demo in 1 minute.

Top comments (0)