In this comprehensive tutorial, we'll learn how to build modern full-stack applications using two powerful technologies: Next.js 15 for the frontend and Golang with Fiber for the backend.
Before starting, make sure you're familiar with:
The Next.js + Golang combination offers:
Let's start by creating a clean and scalable project structure:
mkdir fullstack-nextjs-golang
cd fullstack-nextjs-golang
mkdir frontend backend
Your final structure will look like:
fullstack-nextjs-golang/
├── frontend/ # Next.js application
├── backend/ # Golang API server
├── docker-compose.yml # Local development setup
└── README.md
First, let's set up the Next.js frontend with all the modern features:
cd frontend
npx create-next-app@latest . --typescript --tailwind --app
Install additional packages for a robust frontend:
npm install @tanstack/react-query axios zod react-hook-form @hookform/resolvers
npm install -D @types/node
Update your next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
API_URL: process.env.API_URL || "http://localhost:8080",
},
};
module.exports = nextConfig;
Now let's set up the Golang backend with Fiber framework:
cd ../backend
go mod init backend
go get github.com/gofiber/fiber/v2
go get github.com/gofiber/fiber/v2/middleware/cors
go get github.com/gofiber/fiber/v2/middleware/logger
For the database, we'll use PostgreSQL with GORM:
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/joho/godotenv
Create main.go
:
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
)
func main() {
app := fiber.New()
// Middleware
app.Use(logger.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:3000",
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
}))
// Routes
api := app.Group("/api")
api.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
"message": "Server is running",
})
})
log.Fatal(app.Listen(":8080"))
}
Define your data structures:
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"unique;not null"`
Name string `json:"name" gorm:"not null"`
Password string `json:"-" gorm:"not null"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
type Post struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"not null"`
Content string `json:"content" gorm:"type:text"`
UserID uint `json:"user_id"`
User User `json:"user" gorm:"foreignKey:UserID"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
Implement JWT authentication:
go get github.com/golang-jwt/jwt/v5
go get golang.org/x/crypto/bcrypt
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func AuthRequired() fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(401).JSON(fiber.Map{
"error": "Authorization header required",
})
}
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("your-secret-key"), nil
})
if err != nil || !token.Valid {
return c.Status(401).JSON(fiber.Map{
"error": "Invalid token",
})
}
claims := token.Claims.(jwt.MapClaims)
c.Locals("user_id", claims["user_id"])
return c.Next()
}
}
Create a comprehensive API structure:
POST /api/auth/register
- User registrationPOST /api/auth/login
- User loginGET /api/auth/me
- Get current user (protected)GET /api/posts
- Get all postsPOST /api/posts
- Create post (protected)PUT /api/posts/:id
- Update post (protected)DELETE /api/posts/:id
- Delete post (protected)Create an axios instance:
// lib/api.ts
import axios from "axios";
const api = axios.create({
baseURL: process.env.API_URL || "http://localhost:8080/api",
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default api;
// contexts/AuthContext.tsx
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import api from "@/lib/api";
interface User {
id: number;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
api
.get("/auth/me")
.then((response) => setUser(response.data))
.catch(() => localStorage.removeItem("token"))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email: string, password: string) => {
const response = await api.post("/auth/login", { email, password });
const { token, user } = response.data;
localStorage.setItem("token", token);
setUser(user);
};
const logout = () => {
localStorage.removeItem("token");
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
};
Create docker-compose.yml
for local development:
version: "3.8"
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: fullstack_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
backend:
build: ./backend
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://postgres:password@postgres:5432/fullstack_app?sslmode=disable
depends_on:
- postgres
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
API_URL: http://backend:8080
depends_on:
- backend
volumes:
postgres_data:
// tests/user_test.go
package tests
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateUser(t *testing.T) {
// Test user creation logic
assert.True(t, true)
}
// __tests__/auth.test.tsx
import { render, screen } from "@testing-library/react";
import LoginForm from "@/components/LoginForm";
test("renders login form", () => {
render(<LoginForm />);
expect(screen.getByText("Login")).toBeInTheDocument();
});
Frontend (.env.local
):
API_URL=https://your-api-domain.com
Backend (.env
):
DATABASE_URL=postgres://user:password@host:port/dbname
JWT_SECRET=your-super-secret-jwt-key
This Next.js + Golang stack provides an excellent foundation for building modern, scalable web applications. The combination offers:
Happy coding! 🚀