Your First Application

Build a complete REST API quickly

Build a simple REST API to learn Rivaas basics. You’ll create a working application with multiple routes, JSON responses, and graceful shutdown.

Create Your Project

Create a new directory and initialize a Go module:

mkdir hello-rivaas
cd hello-rivaas
go mod init example.com/hello-rivaas

Install Rivaas

go get rivaas.dev/app

Write Your Application

Create a file named main.go:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"

    "rivaas.dev/app"
)

func main() {
    // Create a new Rivaas application
    a := app.MustNew(
        app.WithServiceName("hello-rivaas"),
        app.WithServiceVersion("v1.0.0"),
    )

    // Define routes
    a.GET("/", handleRoot)
    a.GET("/hello/:name", handleHello)
    a.POST("/echo", handleEcho)

    // Setup graceful shutdown
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer cancel()

    // Start the server
    log.Println("🚀 Starting server on http://localhost:8080")
    if err := a.Start(ctx); err != nil {
        log.Fatal(err)
    }
}

// handleRoot returns a welcome message
func handleRoot(c *app.Context) {
    c.JSON(http.StatusOK, map[string]string{
        "message": "Welcome to Rivaas!",
        "version": "v1.0.0",
    })
}

// handleHello greets a user by name
func handleHello(c *app.Context) {
    name := c.Param("name")
    c.JSON(http.StatusOK, map[string]string{
        "message": "Hello, " + name + "!",
    })
}

// handleEcho echoes back the request body
func handleEcho(c *app.Context) {
    var body map[string]any
    if err := c.Bind(&body); err != nil {
        c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Invalid JSON",
        })
        return
    }

    c.JSON(http.StatusOK, map[string]any{
        "echo": body,
    })
}

Run Your Application

Start the server:

go run main.go

You should see output like:

🚀 Starting server on http://localhost:8080

Test Your API

Open a new terminal and test the endpoints:

Test the root endpoint

curl http://localhost:8080/

Response:

{
  "message": "Welcome to Rivaas!",
  "version": "v1.0.0"
}

Test the greeting endpoint

curl http://localhost:8080/hello/World

Response:

{
  "message": "Hello, World!"
}

Test the echo endpoint

curl -X POST http://localhost:8080/echo \
  -H "Content-Type: application/json" \
  -d '{"name": "Rivaas", "type": "framework"}'

Response:

{
  "echo": {
    "name": "Rivaas",
    "type": "framework"
  }
}

Understanding the Code

Here’s what each part does:

1. Creating the Application

a := app.MustNew(
    app.WithServiceName("hello-rivaas"),
    app.WithServiceVersion("v1.0.0"),
)
  • MustNew() creates a new application. Panics on error. Use in main() functions.
  • WithServiceName() sets the service name.
  • WithServiceVersion() sets the version.

2. Defining Routes

a.GET("/", handleRoot)
a.GET("/hello/:name", handleHello)
a.POST("/echo", handleEcho)
  • GET() and POST() register route handlers.
  • :name is a path parameter. Access it with c.Param("name").
  • Handler functions receive an *app.Context with all request data.

3. Graceful Shutdown

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

if err := a.Start(ctx); err != nil {
    log.Fatal(err)
}
  • signal.NotifyContext() creates a context that cancels on SIGINT (Ctrl+C) or SIGTERM.
  • Start() starts the server and blocks until the context is canceled.
  • The server shuts down gracefully. It finishes active requests before stopping.

4. Handler Functions

func handleRoot(c *app.Context) {
    c.JSON(http.StatusOK, map[string]string{
        "message": "Welcome to Rivaas!",
    })
}
  • Handlers receive an *app.Context.
  • c.JSON() sends a JSON response.
  • c.Param() gets path parameters.
  • c.Bind() parses request bodies. It auto-detects JSON, form, and other formats.

Common Patterns

Path Parameters

// Route: /users/:id/posts/:postId
a.GET("/users/:id/posts/:postId", func(c *app.Context) {
    userID := c.Param("id")
    postID := c.Param("postId")
    
    c.JSON(http.StatusOK, map[string]string{
        "user_id": userID,
        "post_id": postID,
    })
})

Query Parameters

// Route: /search?q=rivaas&limit=10
a.GET("/search", func(c *app.Context) {
    query := c.Query("q")
    limit := c.QueryDefault("limit", "20")
    
    c.JSON(http.StatusOK, map[string]string{
        "query": query,
        "limit": limit,
    })
})

Request Headers

a.GET("/headers", func(c *app.Context) {
    userAgent := c.Request.Header.Get("User-Agent")
    
    c.JSON(http.StatusOK, map[string]string{
        "user_agent": userAgent,
    })
})

Different Status Codes

a.GET("/not-found", func(c *app.Context) {
    c.JSON(http.StatusNotFound, map[string]string{
        "error": "Resource not found",
    })
})

a.POST("/created", func(c *app.Context) {
    c.JSON(http.StatusCreated, map[string]string{
        "message": "Resource created",
    })
})

Testing Your Application

Rivaas provides testing utilities for integration tests:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "rivaas.dev/app"
)

func TestHelloEndpoint(t *testing.T) {
    // Create test app
    a, err := app.New()
    if err != nil {
        t.Fatalf("Failed to create app: %v", err)
    }

    a.GET("/hello/:name", handleHello)

    // Create test request
    req := httptest.NewRequest(http.MethodGet, "/hello/Gopher", nil)
    
    // Test the request
    resp, err := a.Test(req)
    if err != nil {
        t.Fatalf("Request failed: %v", err)
    }
    defer resp.Body.Close()

    // Check status code
    if resp.StatusCode != http.StatusOK {
        t.Errorf("Expected status 200, got %d", resp.StatusCode)
    }
}

Key Testing Methods:

  • a.Test(req) - Execute a request without starting the server
  • a.TestJSON(method, path, body) - Test JSON endpoints
  • app.ExpectJSON(t, resp, status, target) - Verify JSON responses

See the blog example for comprehensive testing patterns.

Common Mistakes

Forgetting Error Handling

// ❌ Bad: Ignoring errors
a := app.MustNew()  // Panics on error

// ✅ Good: Handle errors properly
a, err := app.New()
if err != nil {
    log.Fatalf("Failed to create app: %v", err)
}

Not Using Context for Shutdown

// ❌ Bad: No graceful shutdown
a.Start(context.Background())

// ✅ Good: Graceful shutdown with signals
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
a.Start(ctx)

Registering Routes After Start

// ❌ Bad: Routes registered after Start
a.Start(ctx)
a.GET("/late", handler)  // Won't work!

// ✅ Good: Routes before Start
a.GET("/early", handler)
a.Start(ctx)

Production Basics

Before deploying your first application:

  • ✅ Use environment-based configuration (see Configuration)
  • ✅ Add health endpoints for Kubernetes/Docker
  • ✅ Enable structured logging
  • ✅ Set appropriate timeouts
  • ✅ Add recovery middleware (included by default)

Quick Production Setup:

a, err := app.New(
    app.WithServiceName("my-api"),
    app.WithServiceVersion("v1.0.0"),
    app.WithEnvironment("production"),
    app.WithHealthEndpoints(
        app.WithReadinessCheck("ready", func(ctx context.Context) error {
            return nil // Add real checks here
        }),
    ),
)

See the full-featured example for production patterns.

What’s Next?

You now have a working Rivaas application. Here are the next steps:

Complete Example

The complete code is available in the examples repository.

Troubleshooting

Port Already in Use

If you see “address already in use” (default port is 8080 for HTTP, 8443 for TLS/mTLS):

# Find what's using the port
lsof -i :8080
# Or for TLS/mTLS: lsof -i :8443

# Kill the process or use a different port

Change the port when creating the app:

a, err := app.New(
    app.WithServiceName("my-api"),
    app.WithPort(3000),  // Use port 3000 instead of default 8080
)
// ...
a.Start(ctx)

JSON Binding Errors

If Bind() fails for JSON requests, ensure:

  1. Content-Type header is set to application/json
  2. Request body contains valid JSON
  3. JSON structure matches your Go struct

Ready to learn more? Continue to Configuration →