A schema-first REST API code generator for Go. Define your API in a GraphQL-like SDL and generate idiomatic Chi router handlers, request/response types, and boilerplate.
- Schema-first development — Define endpoints and types in
.sdlor.graphqlfiles - Chi router generation — Produces clean, idiomatic Go handlers
- Type generation — Generates request/response structs with proper JSON tags
- Merge on regeneration — Preserves your handler implementations when regenerating
- Include system — Share types across schemas with namespaced imports
- Nullable semantics — Follows GraphQL conventions (
Type= nullable,Type!= required)
go install github.com/borderlesshq/restgen@latest# Initialize a new project
restgen init
# Edit schemas/example.sdl, then generate
restgen generate# @base("/v1/contacts")
# @models("github.com/yourorg/yourapp/models")
type Calls {
createContact(input: CreateContactInput!): Contact! @post("/")
getContact(id: ID!): Contact @get("/{id}")
updateContact(id: ID!, input: UpdateContactInput!): Contact! @put("/{id}")
deleteContact(id: ID!): DeleteResult! @delete("/{id}")
listContacts(filter: ContactFilter): ContactList! @get("/")
}
type Contact {
id: ID!
name: String!
email: String!
createdAt: Time!
}
input CreateContactInput {
name: String!
email: String!
}
input UpdateContactInput {
name: String
email: String
}| Directive | Description |
|---|---|
@base("/path") |
Base path for all routes in this schema |
@models("pkg/path") |
Go package path for generated types |
@include("other.sdl") |
Import types from another schema |
@get, @post, @put, @patch, @delete |
HTTP method + path |
Scalars (configured in restgen.yaml):
| SDL Type | Go Type |
|---|---|
String |
string |
Int |
int |
Float |
float64 |
Boolean |
bool |
ID |
string |
Time |
time.Time |
Nullability (follows GraphQL semantics):
name: String! # required → string
name: String # nullable → *string
items: [Item!]! # required list of required items → []Item
items: [Item] # nullable list of nullable items → *[]*ItemParameters are automatically routed based on HTTP method:
POST/PUT/PATCH (body methods):
{param}in path → path parameter- Single remaining arg → JSON request body
- Multiple non-path args → validation error
GET/DELETE (query methods):
{param}in path → path parameter- Remaining args → query parameters
- Complex types decoded with
gorilla/schema
# Path: id, Body: input
updateContact(id: ID!, input: UpdateContactInput!): Contact! @put("/{id}")
# Path: iso2, stateCode, Body: location
updateLocation(iso2: String!, stateCode: String!, location: LocationInput!): Location!
@put("/locations/{iso2}/states/{stateCode}")
# Path: none, Query: filter (complex type, uses gorilla/schema)
listContacts(filter: ContactFilter): ContactList! @get("/")
# Path: id, Query: format (scalar)
getContact(id: ID!, format: String): Contact @get("/{id}")Share types across schemas using protobuf-style imports:
# geo.sdl
# @models("github.com/yourorg/yourapp/models/geo")
type Location {
lat: Float!
lng: Float!
}
input LocationInput {
lat: Float!
lng: Float!
}# contacts.sdl
# @base("/v1/contacts")
# @models("github.com/yourorg/yourapp/models")
# @include("geo.sdl")
type Contact {
id: ID!
name: String!
location: geo.Location! # namespaced reference
backupLocation: geo.Location # nullable
}
type Calls {
updateLocation(id: ID!, loc: geo.LocationInput!): geo.Location! @put("/{id}/location")
}Generated imports:
import (
models "github.com/yourorg/yourapp/models"
geo "github.com/yourorg/yourapp/models/geo"
)restgen.yaml:
# Package name for generated routes
package: routes
# Output directory for routes
output: ./routes
# Scalar type mappings
scalars:
Time: time.Time
ID: string
Decimal: decimal.Decimal
# Schema file patterns
schemas:
- schemas/*.sdl| File | Regenerated | Purpose |
|---|---|---|
routes/dependencies.go |
No (created once) | Your With* param functions, helpers |
routes/*_routes.go |
Yes (merged) | Handlers, routes, middleware |
models/*_types.go |
Yes | Request/response structs |
// Generated handler with functional options pattern
type ContactsHandler struct {
// add dependencies here (preserved on regen), always remove this whole comment line when you add dependencies to your handler struct.
}
type ContactsParam func(*ContactsHandler)
func NewContactsHandler(params ...ContactsParam) *ContactsHandler {
h := &ContactsHandler{}
for _, param := range params {
param(h)
}
shared.AssertDependencies(*h, "NewContactsHandler")
return h
}
func (h *ContactsHandler) Routes() chi.Router {
r := chi.NewRouter()
h.applyMiddleware(r)
r.Post("/", h.CreateContact)
r.Get("/{id}", h.GetContact)
// ...
return r
}- Add fields to the handler struct in
*_routes.go:
type ContactsHandler struct {
db *sql.DB
logger *slog.Logger
}- Add param functions in
dependencies.go:
func WithDB(db *sql.DB) ContactsParam {
return func(h *ContactsHandler) {
h.db = db
}
}
func WithLogger(logger *slog.Logger) ContactsParam {
return func(h *ContactsHandler) {
h.logger = logger
}
}- Use in your application:
handler := routes.NewContactsHandler(
routes.WithDB(db),
routes.WithLogger(logger),
)
r := chi.NewRouter()
r.Mount("/v1/contacts", handler.Routes())Handler stubs are generated below the marker. Implement them and they'll be preserved:
// --- RESTGEN MARKER (do not edit above) ---
func (h *ContactsHandler) CreateContact(w http.ResponseWriter, r *http.Request) {
var input models.CreateContactInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
shared.WriteResponse(w, http.StatusBadRequest, &shared.ApiResponse[models.Contact]{
Message: err.Error(),
})
return
}
contact, err := h.db.CreateContact(r.Context(), input)
if err != nil {
shared.WriteResponse(w, http.StatusInternalServerError, &shared.ApiResponse[models.Contact]{
Message: err.Error(),
})
return
}
shared.WriteResponse(w, http.StatusCreated, &shared.ApiResponse[models.Contact]{
Data: contact,
Success: true,
})
}Return types follow nullability rules:
getContact(id: ID!): Contact # nullable return
createContact(input: CreateContactInput!): Contact! # required returnGenerated response types:
// Nullable return → pointer generic param
shared.ApiResponse[*models.Contact]
// Required return → value generic param
shared.ApiResponse[models.Contact]The shared package provides common utilities:
// Generic API response wrapper
type ApiResponse[T any] struct {
Data T `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Success bool `json:"success"`
}
// Write JSON response
func WriteResponse[T any](w http.ResponseWriter, statusCode int, response *ApiResponse[T])
// Validate all exported pointer/interface fields are non-nil
func AssertDependencies(h any, constructor string)# Initialize new project with example config and schema
restgen init
# Generate code from schemas
restgen generate
restgen generate -c custom-config.yaml
# Show version
restgen versionWhen regenerating, restgen preserves:
- ✅ Handler struct fields (in
*_routes.go) - ✅ Handler method implementations (below the marker)
- ✅
applyMiddlewarecustomizations - ✅
RouteMiddlewarecustomizations - ✅ Everything in
dependencies.go
Removed endpoints are moved to a commented "REMOVED HANDLERS" section.
MIT