Courses/API-Only Masterclass
Standalone Course~30 min14 challenges

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.

Headless API: An API that has no built-in user interface. It only serves JSON data over HTTP. The frontend is built separately (or doesn't exist at all). The API is the product — everything else consumes it. This is sometimes called "API-first" or "backend-only" architecture.

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
If you're unsure whether to go API-only, ask yourself: "Will I build the frontend with React in this same repo?" If yes, use the default triple architecture. If no (or if the frontend doesn't exist yet), go API-only.
1

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:

Terminal
grit new taskapi --api

The --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:

Project Structure
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.sum

Compare this to a "triple" project (the default): no apps/web, no apps/admin, no packages/shared. The API stands alone.

Even in API-only mode, you still get GORM Studio (/studio), API docs (/docs), Sentinel (/sentinel/ui), and Pulse (/pulse/ui). These are part of the Go API, not the frontend.
2

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:

FolderPurposeKey Files
cmd/server/Application entry pointmain.go
internal/config/Environment variables → Go structconfig.go
internal/database/DB connection, migrations, seedingdatabase.go
internal/model/GORM struct definitionsuser.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 registrationrouter.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.

Request Flow
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 Response
3

Challenge: 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.

Terminal
# 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.go

You should see output like:

Output
[GIN-debug] Listening and serving HTTP on :8080
Connected to PostgreSQL
Connected to Redis
GORM Studio available at /studio
API Docs available at /docs

Verify everything is working:

Terminal
# Health check
curl localhost:8080/pulse/health

# Expected response:
# {"status":"ok","database":"connected","redis":"connected"}
If the API fails to start, check that Docker services are running with docker compose ps. The most common issue is PostgreSQL not being ready yet — wait a few seconds and try again.
4

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.

Resource: A data entity in your API — something you can Create, Read, Update, and Delete (CRUD). Examples: User, Task, Product, Order. Each resource gets a database table, a Go model, a service layer, HTTP handlers, and API routes.

Let's generate two resources for our task API:

Terminal
# 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:

  • Modelinternal/model/task.go with GORM struct and field tags
  • Serviceinternal/service/task_service.go with CRUD operations + pagination + filtering
  • Handlerinternal/handler/task_handler.go with 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:

FieldGo TypeDB Column
title:stringstringVARCHAR NOT NULL
description:textstringTEXT NOT NULL
priority:intintINTEGER NOT NULL
done:boolboolBOOLEAN NOT NULL
due_date:date:optional*time.TimeDATE NULL
name:string:uniquestringVARCHAR NOT NULL UNIQUE
5

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:

Create a Category
POST /api/categories
Content-Type: application/json

{
  "name": "Work",
  "color": "#6c5ce7"
}
Create a Task
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:

List with Pagination
# 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=asc
Every list endpoint automatically supports pagination (page, page_size), sorting (sort, order), and filtering by any field. Grit generates all of this — you don't need to write pagination logic.
6

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.

belongs_to Relationship: A database relationship where one record references another. A Task "belongs to" a Category — meaning the tasks table has a 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:

Terminal
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_id column (foreign key) to the tasks table
  • Add a Category field 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:

Create a Task with Category
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:

Response
{
  "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"
    }
  }
}
7

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.

JWT (JSON Web Token): A self-contained token that encodes user identity. It's a signed string that contains the user's ID, email, and role. The server validates the signature without checking a database — making JWT auth stateless and fast. Tokens expire after a set time (e.g., 24 hours).

The authentication flow:

1. Register a User
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"}
2. Log In
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":{...}}}
3. Use the Token on Protected Endpoints
curl localhost:8080/api/tasks \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# Without the token, protected endpoints return 401 Unauthorized

The 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.

In the API docs UI at /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.
8

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/tasks without a token — what error do you get?
  • Call GET /api/tasks with 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.

RBAC (Role-Based Access Control): A security model where permissions are assigned to roles, and roles are assigned to users. Instead of giving each user individual permissions, you assign them a role (like USER or ADMIN), and the role determines what they can access.

By default, all generated resource routes require authentication but allow any role. You can restrict routes to admin-only by adding the --roles flag:

Admin-Only Resource
# 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:

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".

9

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.

Deploy Command
grit deploy --host deploy@server.com --domain api.myapp.com

What 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.

For an API-only project, deployment is especially clean. There's no frontend to build, no static files to serve. It's just a single Go binary behind a reverse proxy. The entire deployment takes under 2 minutes.
10

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
11

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
12

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?
13

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?

14

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.