grx

Examples

Copy-paste starters, subscription wiring, router mounts - with the full Go source on the page.

This section shows complete applications that you can run from the repository root. Start with the basic server if you are learning grx. Then choose an example that matches your application, such as authentication, subscriptions, or a custom transport mount.

Every example keeps the same shape:

  1. Define a schema with Schema().
  2. Create a server with grx.NewServer.
  3. Mount the returned handler in your HTTP service.
  4. Add host concerns such as authentication, CORS, logging, and deployment policy around the GraphQL runtime.

The snippets below mirror the Go source under examples/, so you can use them as working references rather than isolated API fragments.

Runs from repo root

The commands assume your working directory is a clone of github.com/grx-gql/grx. Prefer your own module path when you copy snippets into another module (everything under examples/*/ is importable paths from this repo).


Starter servers

Basic GraphQL HTTP server

GraphiQL at GET /, POST /graphql for queries and mutations - the smallest complete loop.

examples/basic/graph/schema.go

package graph

import "github.com/grx-gql/grx/schema"

type Query struct{}

func Schema() schema.Config {
	return schema.Config{
		Query: Query{},
	}
}

examples/basic/graph/user.go

package graph

import (
	"context"
)

type User struct {
	ID    string  `gql:"id,nonNull"`
	Name  string  `gql:"name,nonNull"`
	Email *string `gql:"email"`
}

type UserArgs struct {
	ID string `gql:"id,nonNull"`
}

func (Query) User(ctx context.Context, args UserArgs) (*User, error) {
	email := "ada@example.com"
	return &User{ID: args.ID, Name: "Ada Lovelace", Email: &email}, nil
}

examples/basic/main.go

package main

import (
	"log"
	"net/http"

	"github.com/grx-gql/grx"
	"github.com/grx-gql/grx/examples/basic/graph"
)

func main() {
	srv, err := grx.NewServer(
		grx.WithSchema(graph.Schema()),
		grx.WithPlaygroundPath("/"),
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("GraphQL playground: http://localhost:4000/")
	log.Println("GraphQL endpoint: POST http://localhost:4000/graphql")
	log.Fatal(http.ListenAndServe(":4000", srv))
}
go run ./examples/basic

Try inside GraphiQL:

{
  user(id: "user_1") {
    id
    name
    email
  }
}

Next concepts: Run your first server · Queries and mutations


Bearer token + field authorizer

Shows how host HTTP middleware turns Authorization: Bearer … into a request-scoped subject, and how WithFieldAuthorizer blocks viewer until that subject exists - the same narrative as Authentication & authorization. Runs on http://localhost:4010/ with playground at /.

examples/auth/session/context.go

package session

import (
	"context"
)

type subjectKey struct{}

func ContextWithSubject(ctx context.Context, subject string) context.Context {
	return context.WithValue(ctx, subjectKey{}, subject)
}

func Subject(ctx context.Context) (string, bool) {
	s, ok := ctx.Value(subjectKey{}).(string)
	if !ok || s == "" {
		return "", false
	}
	return s, true
}

examples/auth/graph/schema.go

package graph

import (
	"context"
	"errors"

	"github.com/grx-gql/grx/examples/auth/session"
	"github.com/grx-gql/grx/schema"
)

type Query struct{}

type User struct {
	ID          string `gql:"id,nonNull"`
	DisplayName string `gql:"displayName,nonNull"`
}

func Schema() schema.Config {
	return schema.Config{
		Query: Query{},
	}
}

func (Query) Ping() string {
	return "ok"
}

func (Query) Viewer(ctx context.Context) (*User, error) {
	sub, ok := session.Subject(ctx)
	if !ok {
		return nil, errors.New("unauthenticated")
	}
	return &User{ID: sub, DisplayName: "Signed-in subject " + sub}, nil
}

examples/auth/main.go

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/grx-gql/grx"
	"github.com/grx-gql/grx/examples/auth/graph"
	"github.com/grx-gql/grx/examples/auth/session"
)

const demoBearerToken = "demo-alice-token"

func parseBearer(header string) (token string, ok bool) {
	parts := strings.Fields(strings.TrimSpace(header))
	if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
		return "", false
	}
	return parts[1], true
}

func bearerAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		token, hasBearerScheme := parseBearer(r.Header.Get("Authorization"))
		if hasBearerScheme {
			switch token {
			case demoBearerToken:
				ctx = session.ContextWithSubject(ctx, "alice")
			case "":
				http.Error(w, `invalid Authorization header`, http.StatusBadRequest)
				return
			default:
				http.Error(w, `invalid bearer token`, http.StatusUnauthorized)
				return
			}
		}

		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func requireViewerField() grx.FieldAuthorizer {
	return func(ctx context.Context, fc grx.FieldAuthorizationContext) error {
		if fc.ParentType != "Query" || fc.FieldName != "viewer" {
			return nil
		}
		if _, ok := session.Subject(ctx); !ok {
			return fmt.Errorf("viewer requires Authorization: Bearer %s", demoBearerToken)
		}
		return nil
	}
}

func main() {
	srv, err := grx.NewServer(
		grx.WithSchema(graph.Schema()),
		grx.WithPlaygroundPath("/"),
		grx.WithFieldAuthorizer(requireViewerField()),
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Println(`GraphQL playground: http://localhost:4010/`)
	log.Println(`For { viewer { id } } add header Authorization: Bearer ` + demoBearerToken)
	log.Fatal(http.ListenAndServe(":4010", bearerAuth(srv)))
}
go run ./examples/auth

GraphiQL Headers pane:

{ "Authorization": "Bearer demo-alice-token" }

Then query:

{
  viewer {
    id
    displayName
  }
}

Guide: Authentication & authorization


Subscriptions (WebSocket + SSE)

Memory pub/sub feeds graphql-transport-ws - plus bundled SSE. Configure browser CORS at the host HTTP layer as shown in CORS & browsers.

examples/subscriptions/main.go

package main

import (
	"log"
	"log/slog"
	"net/http"
	"os"

	"github.com/grx-gql/grx"
	"github.com/grx-gql/grx/examples/subscriptions/graph"
	"github.com/grx-gql/grx/plugins/logger"
	"github.com/grx-gql/grx/pubsub/memory"
	"github.com/grx-gql/grx/sse"
	"github.com/grx-gql/grx/websocket"
)

const (
	port           = "4000"
	playgroundPath = "/"
)

func main() {
	loggerPlugin, err := logger.New(logger.Config{
		Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})),
	})
	if err != nil {
		log.Fatal(err)
	}

	bus := memory.New()
	defer bus.Close()

	srv, err := grx.NewServer(
		grx.WithSchema(graph.Schema(bus)),
		grx.WithPlugins(loggerPlugin),
		grx.WithPlaygroundPath(playgroundPath),
		grx.WithTransports(
			websocket.New(),
			sse.New(),
		),
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Server is running on port ", port)
	log.Println("GraphQL Playground is available at http://localhost:" + port + srv.PlaygroundPath)
	log.Println("GraphQL HTTP (queries/mutations): POST http://localhost:" + port + srv.GraphqlPath)
	log.Println("Subscriptions (WebSocket/SSE): http://localhost:" + port + srv.SubscriptionPath)
	log.Fatal(http.ListenAndServe(":"+port, srv))
}

With graph.Schema(nil), only queries and createPost are exposed. Passing a bus with graph.Schema(bus) also exposes createUser, postMessage, and the subscription root. The snippets below are included from the tested example source.

examples/subscriptions/graph/schema.go

package graph

import (
	"github.com/grx-gql/grx/pubsub"
	"github.com/grx-gql/grx/schema"
)

// Query is the schema's root query type. Each entity contributes its own
// resolver struct (e.g. UserQuery, PostQuery) which is embedded here so its
// methods become fields on the root Query type. To add a new entity, define
// its <Entity>Query struct in its own file and embed it below.
type Query struct {
	UserQuery
	PostQuery
}

// Mutation is the schema's root mutation type. Embed each entity's
// <Entity>Mutation struct to expose its mutation fields. Mutations
// that publish domain events take a pubsub dependency so subscriptions
// can fan the events out to connected clients.
type Mutation struct {
	*UserMutation
	PostMutation
	*MessageMutation
}

// Subscription is the schema's root subscription type. Each entity
// that publishes streams contributes its own <Entity>Subscription
// struct, receiving the same bus instance as the matching mutation
// so events flow end-to-end.
type Subscription struct {
	UserSubscription
	MessageSubscription
}

// Schema composes schema.Config. A nil bus returns the query and non-event
// mutation schema; a bus adds event mutations and subscriptions.
func Schema(bus pubsub.PubSub) schema.Config {
	config := schema.Config{
		Query:    Query{},
		Mutation: PostMutation{},
	}
	if bus == nil {
		return config
	}
	config.Mutation = Mutation{
		PostMutation:    PostMutation{},
		UserMutation:    &UserMutation{Bus: bus},
		MessageMutation: &MessageMutation{Bus: bus},
	}
	config.Subscription = Subscription{
		UserSubscription:    UserSubscription{Bus: bus},
		MessageSubscription: MessageSubscription{Bus: bus},
	}
	return config
}

examples/subscriptions/graph/user.go

package graph

import (
	"context"
	"encoding/json"
	"fmt"
	"sync/atomic"

	"github.com/grx-gql/grx/pubsub"
)

// userCreatedTopic is the bus topic on which User events are published.
// Centralising it here avoids drift between publisher and subscriber.
const userCreatedTopic = "user.created"

type User struct {
	ID    string  `gql:"id,nonNull"`
	Name  string  `gql:"name,nonNull"`
	Email *string `gql:"email"`
}

type UserArgs struct {
	ID string `gql:"id,nonNull"`
}

type UserCreateInput struct {
	Name  string  `gql:"name,nonNull"`
	Email *string `gql:"email"`
}

type UserCreateArgs struct {
	Input UserCreateInput `gql:"input,nonNull"`
}

type UserCreatePayload struct {
	User *User `gql:"user,nonNull"`
}

// UserQuery groups every read-side resolver for the User entity.
type UserQuery struct{}

func (UserQuery) User(ctx context.Context, args UserArgs) (*User, error) {
	email := "ada@example.com"
	return &User{ID: args.ID, Name: "Ada Lovelace", Email: &email}, nil
}

func (UserQuery) ErrorExample(ctx context.Context) (string, error) {
	return "", fmt.Errorf("example error from basic server")
}

// UserMutation groups every write-side resolver for the User entity.
// The Bus dependency is wired by New so the mutation can publish
// domain events that subscriptions consume.
type UserMutation struct {
	Bus    pubsub.PubSub
	nextID atomic.Uint64
}

func (m *UserMutation) CreateUser(ctx context.Context, args UserCreateArgs) (*UserCreatePayload, error) {
	id := m.nextID.Add(1)
	user := &User{
		ID:    fmt.Sprintf("user_%d", id),
		Name:  args.Input.Name,
		Email: args.Input.Email,
	}
	if m.Bus != nil {
		payload, err := json.Marshal(user)
		if err != nil {
			return nil, err
		}
		if err := m.Bus.Publish(ctx, userCreatedTopic, payload); err != nil {
			return nil, err
		}
	}
	return &UserCreatePayload{User: user}, nil
}

// UserSubscription groups every stream resolver for the User entity. The
// Bus dependency is wired by New; UserCreated relays User events
// published by CreateUser to every active GraphQL subscription.
type UserSubscription struct {
	Bus pubsub.PubSub
}

func (s UserSubscription) UserCreated(ctx context.Context) (<-chan *User, error) {
	msgs, err := s.Bus.Subscribe(ctx, userCreatedTopic)
	if err != nil {
		return nil, err
	}
	out := make(chan *User)
	go func() {
		defer close(out)
		for msg := range msgs {
			var user User
			if err := json.Unmarshal(msg.Payload, &user); err != nil {
				continue
			}
			u := user
			select {
			case <-ctx.Done():
				return
			case out <- &u:
			}
		}
	}()
	return out, nil
}

Post provides queries and non-event mutations. Message publishes JSON payloads to a separate topic for each room; its subscription uses the same topic name.

examples/subscriptions/graph/post.go

package graph

import "context"

type Post struct {
	ID     string `gql:"id,nonNull"`
	Title  string `gql:"title,nonNull"`
	Body   string `gql:"body,nonNull"`
	Author *User  `gql:"author,nonNull"`
}

type PostArgs struct {
	ID string `gql:"id,nonNull"`
}

type PostCreateInput struct {
	Title    string `gql:"title,nonNull"`
	Body     string `gql:"body,nonNull"`
	AuthorID string `gql:"authorId,nonNull"`
}

type PostCreateArgs struct {
	Input PostCreateInput `gql:"input,nonNull"`
}

type PostCreatePayload struct {
	Post *Post `gql:"post,nonNull"`
}

type PostQuery struct{}

func (PostQuery) Post(ctx context.Context, args PostArgs) (*Post, error) {
	email := "ada@example.com"
	return &Post{
		ID:     args.ID,
		Title:  "Hello, grx",
		Body:   "Composing root types from per-entity resolver structs.",
		Author: &User{ID: "user_1", Name: "Ada Lovelace", Email: &email},
	}, nil
}

type PostMutation struct{}

func (PostMutation) CreatePost(ctx context.Context, args PostCreateArgs) (*PostCreatePayload, error) {
	post := &Post{
		ID:     "post_1",
		Title:  args.Input.Title,
		Body:   args.Input.Body,
		Author: &User{ID: args.Input.AuthorID, Name: "Ada Lovelace"},
	}
	return &PostCreatePayload{Post: post}, nil
}

examples/subscriptions/graph/message.go

package graph

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	"github.com/grx-gql/grx/pubsub"
)

// messagePostedTopicPrefix is the bus topic prefix for chat message events.
// Each room gets its own topic so subscribers receive only that room's traffic.
const messagePostedTopicPrefix = "message.posted."

func messagePostedTopic(roomID string) string {
	return messagePostedTopicPrefix + roomID
}

type Message struct {
	ID       string `gql:"id,nonNull"`
	RoomID   string `gql:"roomId,nonNull"`
	Author   string `gql:"author,nonNull"`
	Body     string `gql:"body,nonNull"`
	PostedAt string `gql:"postedAt,nonNull"`
}

type PostMessageInput struct {
	RoomID string `gql:"roomId,nonNull"`
	Author string `gql:"author,nonNull"`
	Body   string `gql:"body,nonNull"`
}

type PostMessageArgs struct {
	Input PostMessageInput `gql:"input,nonNull"`
}

type PostMessagePayload struct {
	Message *Message `gql:"message,nonNull"`
}

type MessagePostedArgs struct {
	RoomID string `gql:"roomId,nonNull"`
}

// MessageMutation owns the write side of the Message entity. It assigns
// a monotonic ID, stamps the server time, and publishes the new message
// on the room's topic so subscribers for that room receive it.
type MessageMutation struct {
	Bus    pubsub.PubSub
	nextID atomic.Uint64
}

func (m *MessageMutation) PostMessage(ctx context.Context, args PostMessageArgs) (*PostMessagePayload, error) {
	if strings.TrimSpace(args.Input.RoomID) == "" {
		return nil, fmt.Errorf("roomId is required")
	}
	if strings.TrimSpace(args.Input.Body) == "" {
		return nil, fmt.Errorf("body is required")
	}

	id := m.nextID.Add(1)
	message := &Message{
		ID:       fmt.Sprintf("msg_%d", id),
		RoomID:   args.Input.RoomID,
		Author:   args.Input.Author,
		Body:     args.Input.Body,
		PostedAt: time.Now().UTC().Format(time.RFC3339Nano),
	}
	if m.Bus != nil {
		payload, err := json.Marshal(message)
		if err != nil {
			return nil, err
		}
		if err := m.Bus.Publish(ctx, messagePostedTopic(message.RoomID), payload); err != nil {
			return nil, err
		}
	}
	return &PostMessagePayload{Message: message}, nil
}

// MessageSubscription owns the streaming side of the Message entity.
// MessagePosted subscribes to the room-specific topic so each GraphQL
// subscription receives only messages for its requested roomId.
type MessageSubscription struct {
	Bus pubsub.PubSub
}

func (s MessageSubscription) MessagePosted(ctx context.Context, args MessagePostedArgs) (<-chan *Message, error) {
	if strings.TrimSpace(args.RoomID) == "" {
		return nil, fmt.Errorf("roomId is required")
	}
	msgs, err := s.Bus.Subscribe(ctx, messagePostedTopic(args.RoomID))
	if err != nil {
		return nil, err
	}
	out := make(chan *Message)
	go func() {
		defer close(out)
		for msg := range msgs {
			var message Message
			if err := json.Unmarshal(msg.Payload, &message); err != nil {
				continue
			}
			m := message
			select {
			case <-ctx.Done():
				return
			case out <- &m:
			}
		}
	}()
	return out, nil
}
go run ./examples/subscriptions

Dig deeper: Realtime subscriptions


Subscriptions over SSE only

Registers sse.New() as the only subscription transport, so subscriptions are ordinary HTTP requests answered with a text/event-stream response - no WebSocket upgrade. The bundled playground detects this and subscribes over SSE too.

examples/sse/main.go

package main

import (
	"log"
	"net/http"

	"github.com/grx-gql/grx"
	"github.com/grx-gql/grx/examples/sse/graph"
	"github.com/grx-gql/grx/pubsub/memory"
	"github.com/grx-gql/grx/sse"
)

const port = "4000"

func main() {
	bus := memory.New()
	defer bus.Close()

	// sse.New() is the only subscription transport, so subscriptions are
	// plain HTTP requests answered with a text/event-stream response. The
	// bundled playground detects this and subscribes over SSE as well.
	srv, err := grx.NewServer(
		grx.WithSchema(graph.Schema(bus)),
		grx.WithPlaygroundPath("/"),
		grx.WithTransports(sse.New()),
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("GraphQL playground: http://localhost:" + port + srv.PlaygroundPath)
	log.Println("GraphQL endpoint: POST http://localhost:" + port + srv.GraphqlPath)
	log.Println("Subscriptions (SSE): POST http://localhost:" + port + srv.SubscriptionPath + " with Accept: text/event-stream")
	log.Fatal(http.ListenAndServe(":"+port, srv))
}

countdown produces its own stream and closes it when it reaches zero, which ends the response with a complete event. notifications relays events published by sendNotification through the in-memory bus.

examples/sse/graph/schema.go

package graph

import (
	"context"
	"time"

	"github.com/grx-gql/grx/pubsub"
	"github.com/grx-gql/grx/schema"
)

// Query is the schema's root query type.
type Query struct{}

// ServerTime returns the server's current UTC time.
func (Query) ServerTime(ctx context.Context) (string, error) {
	return time.Now().UTC().Format(time.RFC3339), nil
}

// Subscription is the schema's root subscription type. Countdown streams on
// its own; Notifications relays events published by SendNotification.
type Subscription struct {
	CountdownSubscription
	NotificationSubscription
}

// Schema composes schema.Config. The bus carries notification events from
// the mutation to every active subscription.
func Schema(bus pubsub.PubSub) schema.Config {
	return schema.Config{
		Query:    Query{},
		Mutation: &NotificationMutation{Bus: bus},
		Subscription: Subscription{
			NotificationSubscription: NotificationSubscription{Bus: bus},
		},
	}
}

examples/sse/graph/countdown.go

package graph

import (
	"context"
	"fmt"
	"time"
)

// maxCountdown bounds how long a single countdown stream can stay open.
const maxCountdown = 60

type Countdown struct {
	Remaining int `gql:"remaining,nonNull"`
}

type CountdownArgs struct {
	From int `gql:"from,nonNull"`
}

// CountdownSubscription needs no pub/sub: the resolver produces the stream
// itself and closes the channel when it is done, which ends the SSE response
// with a "complete" event.
type CountdownSubscription struct {
	// Interval is the delay between ticks. Zero means one second.
	Interval time.Duration
}

func (s CountdownSubscription) Countdown(ctx context.Context, args CountdownArgs) (<-chan *Countdown, error) {
	if args.From < 0 || args.From > maxCountdown {
		return nil, fmt.Errorf("from must be between 0 and %d", maxCountdown)
	}
	interval := s.Interval
	if interval <= 0 {
		interval = time.Second
	}

	out := make(chan *Countdown)
	go func() {
		defer close(out)
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for remaining := args.From; remaining >= 0; remaining-- {
			select {
			case <-ctx.Done():
				return
			case out <- &Countdown{Remaining: remaining}:
			}
			if remaining == 0 {
				return
			}
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
			}
		}
	}()
	return out, nil
}

examples/sse/graph/notification.go

package graph

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	"github.com/grx-gql/grx/pubsub"
)

// notificationTopicPrefix is the bus topic prefix for notification events.
// Each channel gets its own topic so subscribers only receive its traffic.
const notificationTopicPrefix = "notification.sent."

func notificationTopic(channel string) string {
	return notificationTopicPrefix + channel
}

type Notification struct {
	ID      string `gql:"id,nonNull"`
	Channel string `gql:"channel,nonNull"`
	Text    string `gql:"text,nonNull"`
	SentAt  string `gql:"sentAt,nonNull"`
}

type SendNotificationInput struct {
	Channel string `gql:"channel,nonNull"`
	Text    string `gql:"text,nonNull"`
}

type SendNotificationArgs struct {
	Input SendNotificationInput `gql:"input,nonNull"`
}

type NotificationsArgs struct {
	Channel string `gql:"channel,nonNull"`
}

// NotificationMutation publishes each notification on its channel's topic.
type NotificationMutation struct {
	Bus    pubsub.PubSub
	nextID atomic.Uint64
}

func (m *NotificationMutation) SendNotification(ctx context.Context, args SendNotificationArgs) (*Notification, error) {
	channel := strings.TrimSpace(args.Input.Channel)
	if channel == "" {
		return nil, fmt.Errorf("channel is required")
	}
	if strings.TrimSpace(args.Input.Text) == "" {
		return nil, fmt.Errorf("text is required")
	}

	notification := &Notification{
		ID:      fmt.Sprintf("notification_%d", m.nextID.Add(1)),
		Channel: channel,
		Text:    args.Input.Text,
		SentAt:  time.Now().UTC().Format(time.RFC3339Nano),
	}
	payload, err := json.Marshal(notification)
	if err != nil {
		return nil, err
	}
	if err := m.Bus.Publish(ctx, notificationTopic(channel), payload); err != nil {
		return nil, err
	}
	return notification, nil
}

// NotificationSubscription relays the notifications published on a channel
// to every client subscribed to it.
type NotificationSubscription struct {
	Bus pubsub.PubSub
}

func (s NotificationSubscription) Notifications(ctx context.Context, args NotificationsArgs) (<-chan *Notification, error) {
	channel := strings.TrimSpace(args.Channel)
	if channel == "" {
		return nil, fmt.Errorf("channel is required")
	}
	msgs, err := s.Bus.Subscribe(ctx, notificationTopic(channel))
	if err != nil {
		return nil, err
	}
	out := make(chan *Notification)
	go func() {
		defer close(out)
		for msg := range msgs {
			var notification Notification
			if err := json.Unmarshal(msg.Payload, &notification); err != nil {
				continue
			}
			select {
			case <-ctx.Done():
				return
			case out <- &notification:
			}
		}
	}()
	return out, nil
}
go run ./examples/sse

Subscribe from a terminal:

curl -N http://localhost:4000/graphql \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -d '{"query":"subscription { countdown(from: 3) { remaining } }"}'

Dig deeper: Realtime subscriptions


Router integrations

grx answers ServeHTTP with exact path matches - you usually pair http.StripPrefix("/api", gql) with WithPlaygroundPath("/playground") and WithGraphQLPath("/query") so callers see /api/playground and POST /api/query.

Build graph any way you like (see **Get started** or Basic HTTP above), then reuse this fragment before each snippet:

	gql, err := grx.NewServer(
		grx.WithSchema(graph.Schema()),
		grx.WithPlaygroundPath("/playground"),
		grx.WithGraphQLPath("/query"),
	)
	if err != nil {
		log.Fatal(err)
	}
	delegated := http.StripPrefix("/api", gql)

Long-form guides: net/http ServeMux · Chi · Gin · Echo · Fiber

The Gin, Echo, and Fiber excerpts below reuse delegated from the fragment above. Chi repeats the full grx.NewServer block for copy-paste convenience.


net/http ServeMux

mux := http.NewServeMux()
mux.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
	w.Write([]byte("ok"))
}))
mux.Handle("/api/", delegated)

Chi

go get github.com/go-chi/chi/v5
package main

import (
	"log"
	"net/http"

	"example.com/hello-grx/graph"
	"github.com/go-chi/chi/v5"
	"github.com/grx-gql/grx"
)

func main() {
	gql, err := grx.NewServer(
		grx.WithSchema(graph.Schema()),
		grx.WithPlaygroundPath("/playground"),
		grx.WithGraphQLPath("/query"),
	)
	if err != nil {
		log.Fatal(err)
	}

	r := chi.NewRouter()
	r.Get("/health", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) })
	r.Mount("/api", http.StripPrefix("/api", gql))

	log.Fatal(http.ListenAndServe(":8080", r))
}

Gin

go get github.com/gin-gonic/gin
	engine := gin.Default()
	engine.GET("/health", func(c *gin.Context) { c.String(200, "ok") })
	engine.Any("/api/*proxyPath", gin.WrapH(delegated))
	log.Fatal(engine.Run(":8080"))

Echo

go get github.com/labstack/echo/v4
	e := echo.New()
	e.GET("/health", func(c echo.Context) error { return c.String(200, "ok") })
	e.Any("/api/*", echo.WrapHandler(delegated))
	log.Fatal(e.Start(":8080"))

Fiber

go get github.com/gofiber/fiber/v2
	app := fiber.New()
	app.Get("/health", func(c *fiber.Ctx) error { return c.SendString("ok") })
	app.All("/api/*", adaptor.HTTPHandler(delegated))
	log.Fatal(app.Listen(":8080"))

(import github.com/gofiber/fiber/v2 and github.com/gofiber/fiber/v2/middleware/adaptor)


Measuring throughput

Micro-benchmarks help compare resolver hot paths - they are not a substitute for profiling your full HTTP/RPS workload.

See Benchmarks for scenarios, methodology, and latest tables.


Source


Doc cross-links

Edit this page on GitHub

Last updated on

On this page