Architecture Modes

Single Architecture: One Binary

One Go binary serves both the API and the frontend. No monorepo tooling, no separate deployments. Build it, ship one file, done.

Overview

The single architecture is the most different from the other Grit modes. Instead of a Turborepo monorepo with separate apps, you get a flat project where a single Go binary serves both the REST API and the compiled React frontend using go:embed. Think of it like Laravel or an embedded Next.js app -- one deployment unit that handles everything.

There is no Turborepo, no pnpm-workspace.yaml, no turbo.json. The Go module path is just myapp (not myapp/apps/api). Schemas and types live directly in frontend/src/ -- no shared package needed.

Scaffold command

grit new myapp --single --vite

Key Characteristics

PropertySingle Architecture
BinaryOne Go binary serves API + frontend
MonorepoNone -- flat project, no Turborepo, no pnpm workspaces
Module pathmyapp
FrontendReact + Vite + TanStack Router (embedded via go:embed)
Shared typesInline in frontend/src/ (no packages/shared)
DeploymentUpload one binary + .env file

Full Folder Structure

The entire project is flat. Go code lives in internal/, React code lives in frontend/, and main.go sits at the root.

myapp/
myapp/
├── main.go # Entry point with go:embed frontend/dist/*
├── go.mod # Module: myapp (not myapp/apps/api)
├── go.sum
├── .env
├── .env.example
├── .gitignore
├── .prettierrc
├── .prettierignore
├── docker-compose.yml # PostgreSQL, Redis, MinIO, Mailhog
├── docker-compose.prod.yml
├── grit.json # architecture: "single", frontend: "tanstack"
├── Makefile # make dev, make build, make deploy
├── .claude/skills/grit/
│ ├── SKILL.md # Tailored to single architecture
│ └── reference.md
├── internal/ # ALL Go backend code
│ ├── config/config.go
│ ├── database/db.go
│ ├── models/ # // grit:models
│ ├── handlers/
│ ├── services/
│ ├── middleware/
│ ├── routes/routes.go # // grit:handlers, grit:routes:*
│ ├── mail/
│ ├── storage/
│ ├── jobs/
│ ├── cache/
│ ├── ai/
│ └── auth/totp.go
└── frontend/ # React + Vite + TanStack Router
├── package.json
├── vite.config.ts # Proxy /api → localhost:8080
├── tailwind.config.ts
├── tsconfig.json
├── index.html
└── src/
├── main.tsx
├── routes/ # TanStack Router file-based routes
│ ├── __root.tsx
│ ├── index.tsx
│ └── ...
├── components/
├── hooks/
├── lib/
├── schemas/ # Zod schemas (inline, not shared package)
└── types/ # TypeScript types (inline)

Directory Breakdown

main.go -- Entry Point

The root main.go file is where everything starts. It uses Go's embed package to bundle the compiled frontend into the Go binary. In production, this means you get a single executable that can serve both the API and the static frontend files.

main.go
package main
import (
"embed"
"io/fs"
"net/http"
"myapp/internal/config"
"myapp/internal/database"
"myapp/internal/routes"
)
//go:embed frontend/dist/*
var frontendFS embed.FS
func main() {
cfg := config.Load()
db := database.Connect(cfg)
// Serve API routes
r := routes.Setup(db, cfg)
// Serve embedded frontend
dist, _ := fs.Sub(frontendFS, "frontend/dist")
r.NoRoute(gin.WrapH(http.FileServer(http.FS(dist))))
r.Run(":" + cfg.Port)
}

The //go:embed frontend/dist/* directive tells the Go compiler to include the entire frontend/dist/ directory inside the compiled binary. The NoRoute handler serves the frontend for any path that doesn't match an API route.

internal/ -- Go Backend

Identical structure to the other architectures. Contains config, database connection, models, handlers, services, middleware, mail, storage, jobs, cache, and AI integration. The only difference is import paths: you import "myapp/internal/models" instead of "myapp/apps/api/internal/models". The routes/routes.go file contains the same code markers (// grit:handlers, // grit:routes:*) for code generation.

frontend/ -- React SPA

A standard React + Vite + TanStack Router application. File-based routing lives in src/routes/. Unlike the triple or double architecture, there is no packages/shared/ directory. Zod schemas live in src/schemas/ and TypeScript types in src/types/ -- everything is self-contained within the frontend directory.

grit.json -- Project Config

Tells the Grit CLI which architecture this project uses. Code generation reads this file to determine where to place generated files and which templates to use.

grit.json
{
"architecture": "single",
"frontend": "tanstack",
"go_module": "myapp"
}

Data Flow

The data flow differs between development and production. In development, you run two processes: Go on port 8080 and Vite on port 5173. Vite proxies API requests to Go. In production, there is only one process: the Go binary serves everything.

Production

production data flow
Browser → Go binary (:8080)
├── /api/* → Gin router → handlers → services → database
└── everything else → serves frontend/dist/ (HTML, JS, CSS)

Development

development data flow
Browser → Vite dev server (:5173)
├── /api/* → proxied to Go (:8080) → handlers → services → database
└── other → Vite HMR (instant refresh)

The Vite proxy is configured in vite.config.ts to forward any request starting with /api to the Go server running on port 8080.

Development Workflow

During development, you run two processes in separate terminals. The Makefile provides a convenience command that runs both at once.

Terminal 1: Go API

# Start the Go API with hot reload
air
# or without air:
go run main.go

Terminal 2: Vite Frontend

cd frontend && pnpm dev

Or use the Makefile

# Runs both Go and Vite concurrently
make dev

Vite Proxy Config

The Vite config proxies API requests so the frontend can call /api/users without worrying about CORS or port differences during development.

frontend/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [TanStackRouterVite(), react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})

Production Build

Building for production is a two-step process: build the frontend, then build the Go binary. The Go compiler embeds the compiled frontend assets into the binary at compile time.

build steps
# Step 1: Build the frontend
cd frontend && pnpm build
# This creates frontend/dist/ with optimized HTML, JS, and CSS
# Step 2: Build the Go binary
cd .. && go build -o myapp main.go
# The binary now contains the embedded frontend
# Step 3: Deploy
scp myapp server:/opt/myapp/
scp .env server:/opt/myapp/
# That's it. One binary + one .env file.

No Node.js runtime needed on the production server. No npm install, no pnpm, no build tools. Just the binary and your environment variables.

Code Generation

When you run grit generate resource in a single-architecture project, the CLI creates files in different locations compared to the monorepo architectures.

Generated FileLocation
Go modelinternal/models/post.go
Go serviceinternal/services/post_service.go
Go handlerinternal/handlers/post_handler.go
Route injectioninternal/routes/routes.go
Zod schemafrontend/src/schemas/post.ts
TypeScript typesfrontend/src/types/post.ts
React Query hooksfrontend/src/hooks/use-posts.ts

Notice that schemas and types go into frontend/src/ directly, not into a packages/shared/ directory. There is no shared package in the single architecture.

Deployment

Deploying a single-architecture app is as simple as it gets. You only need to transfer two files to your server: the compiled binary and your .env file.

Docker

Dockerfile
FROM node:20-alpine AS frontend
WORKDIR /app/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY frontend/ ./
RUN pnpm build
FROM golang:1.21-alpine AS backend
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=frontend /app/frontend/dist ./frontend/dist
RUN CGO_ENABLED=0 go build -o server main.go
FROM alpine:3.19
WORKDIR /app
COPY --from=backend /app/server .
EXPOSE 8080
CMD ["./server"]

Direct Deploy (no Docker)

# Build locally for your server's OS/arch
GOOS=linux GOARCH=amd64 go build -o myapp main.go
# Transfer and run
scp myapp .env yourserver:/opt/myapp/
ssh yourserver 'cd /opt/myapp && ./myapp'

When to Choose Single

Good fit

  • -Solo developers who want the simplest deploy story
  • -Laravel or Rails developers who like one-app-does-everything
  • -Internal tools and admin dashboards
  • -Microservices where each service has its own UI
  • -Apps where SSR and SEO are not critical (SPA is fine)

Not ideal for

  • -SEO-heavy public sites that need server-side rendering
  • -Large teams that need separate frontend and backend deploys
  • -Projects that need a separate admin panel (use triple instead)
  • -Apps that share types with multiple frontend clients

Example Project

The Job Portal example built with the single architecture demonstrates all the patterns described above: go:embed, Vite proxy, TanStack Router, and production Dockerfile.

View Job Portal (Single + Vite) on GitHub →