How To Make HTTP POST Request With JSON Body In Go

Master the fundamentals of crafting HTTP POST requests with JSON payloads using Go's standard library. From basic examples to production-ready patterns.

Why JSON for HTTP Requests in Go

JSON has become the de facto standard for HTTP APIs due to its human-readable format and universal language support. Go's standard library includes the encoding/json package, which provides efficient serialization and deserialization of Go structs to JSON. When combined with the net/http package, you have everything needed to make production-ready HTTP requests with JSON bodies.

The go-to approach involves defining Go structs that match your JSON payload structure, using json.Marshal to convert them to byte slices, and then sending those bytes with appropriate HTTP headers.

Go's HTTP client has evolved significantly since its introduction. The http.Client type provides configurable timeouts, redirect policies, and transport settings. Modern Go applications should avoid the deprecated io/ioutil package and use io and os packages instead.

When building comprehensive web applications, understanding how to properly communicate with APIs using JSON is essential for integrating third-party services, payment gateways, and cloud platforms.

DigitalOcean's tutorial on Go HTTP requests covers the evolution of Go's HTTP client and recommends modern practices for production applications.

Basic POST Request with http.Post

The simplest way to send a JSON body in an HTTP POST request is using the http.Post function from the net/http package. This function accepts a URL, content type, and reader for the request body.

Using http.Post with Raw JSON Strings

For simple cases where you have pre-formatted JSON, you can convert a string to bytes and send it directly:

package main

import (
 "bytes"
 "net/http"
)

func main() {
 url := "https://api.example.com/endpoint"
 jsonData := `{"name": "John", "email": "[email protected]"}`

 resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(jsonData)))
 if err != nil {
 // Handle error
 }
 defer resp.Body.Close()
}

This approach works well for simple payloads but lacks flexibility for complex data structures.

Using http.Post with json.Marshal

For structured data, define a Go struct and marshal it to JSON:

type User struct {
 Name string `json:"name"`
 Email string `json:"email"`
}

func createUser(url string, user User) (*http.Response, error) {
 jsonData, err := json.Marshal(user)
 if err != nil {
 return nil, err
 }

 return http.Post(url, "application/json", bytes.NewBuffer(jsonData))
}

The json.Marshal function automatically converts struct fields to JSON based on the struct tags, handling type conversion and proper formatting.

For teams building scalable backend systems, the combination of struct-based marshaling provides type safety and maintainability for complex API integrations.

Sentry's guide on JSON POST requests in Go demonstrates that the http.Post function is the foundational building block for making HTTP requests with JSON payloads. LogRocket's tutorial on Go HTTP POST requests explains how struct-based marshaling provides type safety and maintainability for complex API integrations.

Advanced POST Requests with http.NewRequest

For production applications, you often need more control over your HTTP requests--custom headers, timeouts, and request configuration. The http.NewRequest function provides this flexibility.

Adding Custom Headers

Many APIs require authentication or content type headers. Here's how to add custom headers to your POST request:

func postWithHeaders(url string, data interface{}) (*http.Response, error) {
 jsonData, err := json.Marshal(data)
 if err != nil {
 return nil, err
 }

 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
 if err != nil {
 return nil, err
 }

 // Set required headers
 req.Header.Set("Content-Type", "application/json")
 req.Header.Set("Authorization", "Bearer your-token-here")
 req.Header.Set("Accept", "application/json")

 // Use default client or create custom one
 client := &http.Client{}
 return client.Do(req)
}

Setting the Content-Type header to "application/json" tells the server how to interpret your request body, while authorization headers enable secure API access.

Configuring Timeouts

Production applications should always include timeouts to prevent hanging requests. Create a client with appropriate timeout settings:

func createAPIClient() *http.Client {
 return &http.Client{
 Timeout: 30 * time.Second, // Overall request timeout
 Transport: &http.Transport{
 MaxIdleConns: 100,
 MaxIdleConnsPerHost: 10,
 IdleConnTimeout: 90 * time.Second,
 },
 }
}

A 30-second timeout is reasonable for most API calls, but adjust based on your use case.

Proper header configuration and timeout settings are essential for building reliable API clients in production environments. When integrating with external APIs for your web applications, these patterns ensure your services remain responsive and secure.

DigitalOcean's tutorial on Go HTTP requests emphasizes that proper header configuration and timeout settings are essential for building reliable API clients in production environments.

Building a JSON API Server in Go

Understanding how to receive JSON POST requests completes the picture. Go's net/http package handles server-side operations with equal ease.

Creating a Handler for JSON POST Requests

type CreateUserRequest struct {
 Name string `json:"name"`
 Email string `json:"email"`
}

type CreateUserResponse struct {
 ID int `json:"id"`
 Name string `json:"name"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
 // Only allow POST method
 if r.Method != http.MethodPost {
 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
 return
 }

 // Set content type
 w.Header().Set("Content-Type", "application/json")

 // Parse request body
 var req CreateUserRequest
 err := json.NewDecoder(r.Body).Decode(&req)
 if err != nil {
 http.Error(w, "Invalid request body", http.StatusBadRequest)
 return
 }

 // Validate input
 if req.Name == "" || req.Email == "" {
 http.Error(w, "Name and email are required", http.StatusBadRequest)
 return
 }

 // Process request (in real app, save to database)
 response := CreateUserResponse{
 ID: 1,
 Name: req.Name,
 }

 // Send response
 json.NewEncoder(w).Encode(response)
}

This handler demonstrates proper request validation, error handling, and response formatting. The pattern of decoding incoming JSON, validating the data, and encoding the response ensures robust API endpoints.

Starting the Server

func main() {
 http.HandleFunc("/users", createUserHandler)

 fmt.Println("Server starting on :8080")
 if err := http.ListenAndServe(":8080", nil); err != nil {
 fmt.Printf("Server error: %v\n", err)
 }
}

For enterprise web applications, building proper JSON API servers is fundamental to creating scalable microservices architectures that communicate effectively.

LogRocket's tutorial on Go HTTP POST requests provides comprehensive examples of building both client and server components for JSON-based API communication.

Error Handling Best Practices

Robust error handling distinguishes production-ready code from quick prototypes. Your error handling strategy should cover network errors, HTTP status codes, and response body validation.

Checking HTTP Status Codes

func postWithErrorHandling(url string, data interface{}) (*CreateUserResponse, error) {
 jsonData, err := json.Marshal(data)
 if err != nil {
 return nil, fmt.Errorf("failed to marshal JSON: %w", err)
 }

 resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
 if err != nil {
 return nil, fmt.Errorf("request failed: %w", err)
 }
 defer resp.Body.Close()

 // Check status code
 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 // Read error response body
 body, _ := io.ReadAll(resp.Body)
 return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
 }

 // Parse successful response
 var result CreateUserResponse
 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
 return nil, fmt.Errorf("failed to decode response: %w", err)
 }

 return &result, nil
}

Always check both network-level errors and HTTP status codes to provide meaningful feedback. Reading the error response body helps diagnose API-specific issues.

Common Status Code Patterns

  • 2xx: Successful requests (200 OK, 201 Created)
  • 4xx: Client errors (400 Bad Request, 401 Unauthorized, 404 Not Found)
  • 5xx: Server errors (500 Internal Server Error, 503 Service Unavailable)

When building reliable backend systems, comprehensive error handling is critical for production applications that depend on external APIs.

DigitalOcean's tutorial on Go HTTP requests stresses that comprehensive error handling is critical for production applications that depend on external APIs.

Performance and Production Considerations

Production applications require thoughtful performance optimization and reliability patterns.

Using Context for Cancellation

Go's context package enables request cancellation and timeout propagation:

func postWithContext(ctx context.Context, url string, data interface{}) (*http.Response, error) {
 jsonData, err := json.Marshal(data)
 if err != nil {
 return nil, err
 }

 req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
 if err != nil {
 return nil, err
 }
 req.Header.Set("Content-Type", "application/json")

 client := &http.Client{}
 return client.Do(req)
}

Context cancellation automatically terminates the request if the parent context is cancelled, preventing resource leaks and ensuring graceful shutdown behavior.

Connection Pooling

The default HTTP client provides reasonable connection pooling, but you can tune it for high-throughput applications:

func createOptimizedClient() *http.Client {
 return &http.Client{
 Timeout: 30 * time.Second,
 Transport: &http.Transport{
 MaxIdleConns: 100,
 MaxIdleConnsPerHost: 10,
 MaxConnsPerHost: 100,
 IdleConnTimeout: 90 * time.Second,
 ExpectContinueTimeout: 1 * time.Second,
 },
 }
}

Connection reuse reduces latency and server load, especially when making many requests to the same API. The MaxConnsPerHost setting limits concurrent connections to prevent overwhelming downstream services.

These patterns are essential for building scalable backends with Go that can handle high volumes of API traffic efficiently.

Summary

Making HTTP POST requests with JSON bodies in Go involves three key steps:

  1. Define data structures with struct tags that match your JSON schema
  2. Marshal to JSON using json.Marshal for efficient conversion
  3. Send requests with appropriate headers and error handling

The net/http package provides both simple http.Post for basic use cases and configurable http.NewRequest for advanced scenarios. Always include proper error handling, timeouts, and context support for production reliability.

The combination of Go's type safety, efficient JSON encoding, and flexible HTTP client makes it an excellent choice for building API clients and servers alike. Whether you're integrating third-party services or building microservices, Go provides the primitives needed for robust network communication.

For teams building modern web applications, mastering these patterns is essential for effective backend development. Go's standard library ensures your applications remain performant and maintainable as scale requirements grow.

Frequently Asked Questions