Build & Deploy a REST API with Go (API-Only Masterclass)
Build a complete REST API from scratch using grit new myapi --api. No frontend, no admin panel — pure Go API. You'll scaffold, generate resources, test with API docs, add authentication and roles, and deploy to production.
When to Go API-Only
Not every project needs a frontend. Sometimes you just need a clean, fast API that other services or apps consume. Grit's --api flag gives you exactly that — a lean Go API with no frontend overhead.
When does API-only make sense?
- • Mobile app backend — your iOS/Android app needs an API, but the frontend is native code, not React
- • Microservice — one service in a larger architecture that handles a specific domain (payments, notifications, search)
- • Third-party integrations — building a public API that other developers consume (like Stripe or Twilio)
- • Separate frontend team — the frontend is built by a different team using their own stack (Vue, Svelte, Flutter)
- • Data pipeline — an API that ingests, transforms, and serves data with no user-facing UI
Challenge: Name 3 API-Only Scenarios
Think of 3 real-world projects where API-only is the right choice. For each one, explain why a frontend would be built separately (or not at all). Example: a weather data service that mobile apps and websites consume.
Scaffold the API
Let's build a task management API. One command gives you the full project:
grit new taskapi --apiThe --api flag tells Grit to skip the frontend entirely. No apps/web, no apps/admin, no turbo.json, no pnpm-workspace.yaml. What you get is a clean Go API project:
taskapi/
├── docker-compose.yml # PostgreSQL, Redis, MinIO, Mailhog
├── .env # Environment variables
├── .gitignore
├── README.md
└── apps/
└── api/
├── cmd/
│ └── server/
│ └── main.go # Entry point
├── internal/
│ ├── config/ # Environment config
│ ├── database/ # DB connection + migrations
│ ├── handler/ # HTTP handlers
│ ├── middleware/ # Auth, CORS, logging, cache
│ ├── model/ # GORM models
│ ├── router/ # Route definitions
│ └── service/ # Business logic
├── go.mod
└── go.sumCompare this to a "triple" project (the default): no apps/web, no apps/admin, no packages/shared. The API stands alone.
/studio), API docs (/docs), Sentinel (/sentinel/ui), and Pulse (/pulse/ui). These are part of the Go API, not the frontend.Challenge: Scaffold and Explore
Run grit new taskapi --api. Open the project in your editor. How many folders are inside apps/api/internal/? List them. Compare the folder count to what a grit new myapp (triple) project would create.
Project Structure Tour
Let's walk through each folder in the API and understand what it does:
| Folder | Purpose | Key Files |
|---|---|---|
| cmd/server/ | Application entry point | main.go |
| internal/config/ | Environment variables → Go struct | config.go |
| internal/database/ | DB connection, migrations, seeding | database.go |
| internal/model/ | GORM struct definitions | user.go, upload.go |
| internal/handler/ | HTTP request handlers (thin layer) | auth_handler.go, user_handler.go |
| internal/service/ | Business logic (called by handlers) | auth_service.go, cache_service.go |
| internal/middleware/ | Request pipeline (auth, CORS, etc.) | auth.go, cors.go, logger.go |
| internal/router/ | Route registration | router.go |
The pattern is simple: handlers receive HTTP requests, validate input, and call services. Services contain business logic, call the database through models, and return results. Handlers format the results as JSON responses. This separation keeps your code testable and organized.
HTTP Request
→ Router (matches URL to handler)
→ Middleware (auth, logging, CORS)
→ Handler (validates input, calls service)
→ Service (business logic, DB queries)
→ Model (GORM struct, database table)
← Service returns result
← Handler formats JSON response
← Middleware adds headers
← Router sends response
HTTP ResponseChallenge: Count the Folders
Open apps/api/internal/. How many folders are there? List each one and write a one-sentence description of what it does. Which folder contains the most files?
Start the API
Starting an API-only project requires two steps: start the infrastructure services (database, Redis, etc.) and then start the Go API itself.
# Step 1: Start infrastructure (PostgreSQL, Redis, MinIO, Mailhog)
cd taskapi
docker compose up -d
# Step 2: Start the Go API
cd apps/api
go run cmd/server/main.goYou should see output like:
[GIN-debug] Listening and serving HTTP on :8080
Connected to PostgreSQL
Connected to Redis
GORM Studio available at /studio
API Docs available at /docsVerify everything is working:
# Health check
curl localhost:8080/pulse/health
# Expected response:
# {"status":"ok","database":"connected","redis":"connected"}docker compose ps. The most common issue is PostgreSQL not being ready yet — wait a few seconds and try again.Challenge: Start and Verify
Start the infrastructure and API. Verify the health check at localhost:8080/pulse/health. Are both database and Redis connected? Open GORM Studio at localhost:8080/studio — how many tables exist in the fresh database?
Generate Resources
This is where the code generator shines. Instead of manually creating models, handlers, services, and routes for each entity, you describe the resource and Grit generates everything.
Let's generate two resources for our task API:
# Generate a Task resource
grit generate resource Task --fields "title:string,description:text,priority:int,done:bool,due_date:date:optional"
# Generate a Category resource
grit generate resource Category --fields "name:string:unique,color:string"For each resource, Grit generates:
- • Model —
internal/model/task.gowith GORM struct and field tags - • Service —
internal/service/task_service.gowith CRUD operations + pagination + filtering - • Handler —
internal/handler/task_handler.gowith HTTP endpoints - • Routes — injected into
internal/router/router.go - • Migration — auto-migrated by GORM on startup
The field type syntax is name:type with optional modifiers:
| Field | Go Type | DB Column |
|---|---|---|
| title:string | string | VARCHAR NOT NULL |
| description:text | string | TEXT NOT NULL |
| priority:int | int | INTEGER NOT NULL |
| done:bool | bool | BOOLEAN NOT NULL |
| due_date:date:optional | *time.Time | DATE NULL |
| name:string:unique | string | VARCHAR NOT NULL UNIQUE |
Challenge: Generate and Inspect
Generate both the Task and Category resources. Then run grit routes to see all registered API endpoints. How many new endpoints were created? List them. Restart the API and check GORM Studio — are the new tables there?
Test with API Docs
Grit's auto-generated API docs at /docs are not just documentation — they're an interactive testing tool. You can send real requests, see real responses, and explore every endpoint without writing a single curl command.
Let's create some test data through the API:
POST /api/categories
Content-Type: application/json
{
"name": "Work",
"color": "#6c5ce7"
}POST /api/tasks
Content-Type: application/json
{
"title": "Finish API course",
"description": "Complete all 14 challenges in the Grit API Masterclass",
"priority": 1,
"done": false,
"due_date": "2026-04-01"
}Once you have data, test the list endpoint with pagination and filtering:
# Page 1, 5 items per page
GET /api/tasks?page=1&page_size=5
# Filter by priority
GET /api/tasks?priority=1
# Sort by due date (ascending)
GET /api/tasks?sort=due_date&order=ascpage, page_size), sorting (sort, order), and filtering by any field. Grit generates all of this — you don't need to write pagination logic.Challenge: Create Test Data
Open /docs and create 3 categories (Work, Personal, Learning) and 10 tasks spread across them. Test pagination with ?page=1&page_size=5 — do you get 5 results on page 1 and 5 on page 2? Sort by priority descending. Does the highest priority appear first?
Add Relationships
Real APIs have relationships between resources. A Task belongs to a Category. An Order has many OrderItems. Grit handles relationships with the belongs_to field type.
category_id column that references the categories table. When you query a Task, you can include its Category data in the response.To add a relationship, regenerate the Task resource with a category_id field:
grit generate resource Task --fields "title:string,description:text,priority:int,done:bool,due_date:date:optional,category_id:belongs_to:Category"The category_id:belongs_to:Category field tells Grit:
- • Add a
category_idcolumn (foreign key) to the tasks table - • Add a
Categoryfield to the Task struct for preloading - • Include the Category data when fetching Tasks (GORM Preload)
Now when you create a task, you include the category_id:
POST /api/tasks
Content-Type: application/json
{
"title": "Review pull request",
"description": "Review the auth middleware changes",
"priority": 2,
"done": false,
"category_id": 1
}And when you fetch the task, the category data comes along:
{
"data": {
"id": 1,
"title": "Review pull request",
"description": "Review the auth middleware changes",
"priority": 2,
"done": false,
"category_id": 1,
"category": {
"id": 1,
"name": "Work",
"color": "#6c5ce7"
}
}
}Challenge: Test Relationships
Regenerate the Task resource with the category_id:belongs_to:Category field. Create 3 categories and 5 tasks, each assigned to a category. Fetch a single task — does the response include the full category object? Fetch the task list — does every task show its category?
Authentication
Every Grit API ships with JWT authentication. Users register, log in, receive a token, and send that token with every request to access protected endpoints.
The authentication flow:
curl -X POST localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@test.com","password":"password123"}'
# Response:
# {"data":{"id":1,"name":"John","email":"john@test.com","role":"USER"},"message":"Registration successful"}curl -X POST localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"john@test.com","password":"password123"}'
# Response:
# {"data":{"token":"eyJhbGciOiJIUzI1NiIs...","refresh_token":"...","user":{...}}}curl localhost:8080/api/tasks \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
# Without the token, protected endpoints return 401 UnauthorizedThe Authorization header uses the Bearer scheme: the word "Bearer" followed by a space and then the token. This is the industry standard for JWT authentication.
/docs, there's an "Authorize" button where you can paste your JWT token. Once set, every request from the docs UI will include the token automatically.Challenge: Full Auth Flow
Using curl or the API docs:
- Register a user with your name and email
- Log in and copy the JWT token
- Call
GET /api/taskswithout a token — what error do you get? - Call
GET /api/taskswith the token — does it work? - Try creating a task with the token — does it associate with your user?
Role-Based Access
Authentication answers "who are you?" Authorization answers "what can you do?"Grit supports role-based access control (RBAC) with two built-in roles: USER and ADMIN.
By default, all generated resource routes require authentication but allow any role. You can restrict routes to admin-only by adding the --roles flag:
# Generate a resource where write operations are admin-only
grit generate resource AuditLog --fields "action:string,user_id:int,details:text" --roles "ADMIN"When a route is restricted to ADMIN, the middleware checks the user's role from the JWT token. If the user has the USER role, they get a 403 Forbidden response:
{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to access this resource"
}
}You can promote a user to ADMIN through GORM Studio — just edit the user's record and change the role field from "USER" to "ADMIN".
Challenge: Test Role Restrictions
Register two users: one regular user and one admin (promote via GORM Studio). Generate an admin-only resource. Log in as the regular user and try to access the admin endpoint. What response do you get? Now log in as the admin — does it work?
Deploy the API
Grit includes a one-command deployment tool. It connects to your server via SSH, builds your Go binary, sets up a systemd service, configures Caddy for HTTPS, and starts everything.
grit deploy --host deploy@server.com --domain api.myapp.comWhat grit deploy does behind the scenes:
- 1. Connects to your server via SSH
- 2. Uploads your project files
- 3. Builds the Go binary on the server (
go build) - 4. Creates a systemd service for auto-restart
- 5. Configures Caddy reverse proxy with automatic HTTPS
- 6. Starts PostgreSQL and Redis via Docker Compose
- 7. Runs database migrations
- 8. Starts the API
Your server needs: Ubuntu 22.04+, Docker, Go 1.21+, and Caddy. The deploy command handles the rest.
Challenge: Write Your Deploy Command
Write the grit deploy command you would use to deploy your taskapi to a server. Include the SSH host and domain. What DNS record would you need to create before deploying? (Hint: an A record pointing your domain to your server's IP address.)
Summary
Here's everything you learned in this course:
- API-only mode (--api) gives you a lean Go API with no frontend overhead
- The project structure follows handler → service → model separation
- grit generate resource creates model, service, handler, and routes in one command
- Field types include string, text, int, bool, date, float, and belongs_to for relationships
- Every list endpoint gets automatic pagination, sorting, and filtering
- JWT authentication with register, login, and token-based access
- Role-based access control with USER and ADMIN roles
- API docs at /docs provide interactive testing without curl
- GORM Studio at /studio lets you browse and edit database records
- grit deploy ships your API to production in under 2 minutes
Challenge: Build a Bookmark API (Part 1)
Time to build something on your own. Create a new API-only project called bookmarkapi. Generate these resources:
- Category — name:string, color:string
- Bookmark — title:string, url:string:unique, description:text:optional, category_id:belongs_to:Category, favorite:bool
Challenge: Build a Bookmark API (Part 2)
Start the API and create test data: 5 categories and 20 bookmarks. Test these queries:
- List all bookmarks with pagination (page_size=5)
- Filter bookmarks by favorite=true
- Sort bookmarks by title ascending
- Get a single bookmark — does it include the category?
Challenge: Build a Bookmark API (Part 3)
Add authentication. Register a user, log in, and use the token to create bookmarks. Try accessing bookmarks without a token. Does the API reject unauthenticated requests?
Challenge: Build a Bookmark API (Part 4)
Write the deploy command for your bookmark API. What domain would you use? What infrastructure does the server need? If you have a server, deploy it for real. Your first production Go API is live.
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.