Multipart Forms

Handle file uploads and form data

Forge provides built-in support for multipart form data, including file uploads. File handling integrates with the Context interface and OpenAPI auto-generates multipart/form-data schemas from struct tags.

Single File Upload

Use ctx.FormFile(name) to receive a single file from a multipart form.

func uploadAvatar(ctx forge.Context) error {
    file, header, err := ctx.FormFile("avatar")
    if err != nil {
        return forge.BadRequest("missing avatar file: " + err.Error())
    }
    defer file.Close()

    // File metadata
    filename := header.Filename  // "photo.jpg"
    size := header.Size          // 1048576 (bytes)
    contentType := header.Header.Get("Content-Type") // "image/jpeg"

    // Read file contents
    data, err := io.ReadAll(file)
    if err != nil {
        return forge.InternalError(err)
    }

    // Save or process the file
    if err := storage.Save(filename, data); err != nil {
        return forge.InternalError(err)
    }

    return ctx.JSON(200, map[string]any{
        "filename": filename,
        "size":     size,
        "type":     contentType,
    })
}

r.POST("/upload/avatar", uploadAvatar)

Multiple File Upload

Use ctx.FormFiles(name) to receive multiple files from the same form field.

func uploadDocuments(ctx forge.Context) error {
    headers, err := ctx.FormFiles("documents")
    if err != nil {
        return forge.BadRequest("missing documents: " + err.Error())
    }

    results := make([]map[string]any, 0, len(headers))

    for _, header := range headers {
        file, err := header.Open()
        if err != nil {
            return forge.InternalError(err)
        }
        defer file.Close()

        data, err := io.ReadAll(file)
        if err != nil {
            return forge.InternalError(err)
        }

        if err := storage.Save(header.Filename, data); err != nil {
            return forge.InternalError(err)
        }

        results = append(results, map[string]any{
            "filename": header.Filename,
            "size":     header.Size,
        })
    }

    return ctx.JSON(200, map[string]any{
        "uploaded": len(results),
        "files":   results,
    })
}

r.POST("/upload/documents", uploadDocuments)

Mixed Form Data and Files

Combine file uploads with regular form fields using ctx.BindForm alongside ctx.FormFile.

type ProfileForm struct {
    Name     string `form:"name"`
    Email    string `form:"email"`
    Bio      string `form:"bio"`
}

func updateProfile(ctx forge.Context) error {
    // Bind text fields
    var form ProfileForm
    if err := ctx.BindForm(&form); err != nil {
        return forge.BadRequest("invalid form data")
    }

    // Handle optional file upload
    file, header, err := ctx.FormFile("avatar")
    var avatarURL string
    if err == nil {
        defer file.Close()
        data, _ := io.ReadAll(file)
        avatarURL, _ = storage.Upload(header.Filename, data)
    }

    return ctx.JSON(200, map[string]any{
        "name":      form.Name,
        "email":     form.Email,
        "bio":       form.Bio,
        "avatarUrl": avatarURL,
    })
}

r.PUT("/profile", updateProfile)

Struct Tags for OpenAPI

Forge auto-generates multipart/form-data OpenAPI schemas from struct tags. Use form tags for regular fields and format:"binary" for file fields.

type UploadRequest struct {
    // Regular form fields
    Title       string `form:"title" description:"Document title" minLength:"1"`
    Category    string `form:"category" description:"Category" enum:"report,invoice,receipt"`

    // File fields
    File        []byte `form:"file" format:"binary" description:"The document file"`
    Thumbnail   []byte `form:"thumbnail" format:"binary" description:"Optional thumbnail"`
}

r.POST("/documents", uploadDocument,
    forge.WithRequestSchema(&UploadRequest{}),
    forge.WithRequestContentTypes("multipart/form-data"),
    forge.WithResponseSchema(201, "Document created", &Document{}),
    forge.WithTags("documents"),
)

When Forge detects form tags in a request schema, it automatically sets the content type to multipart/form-data in the OpenAPI spec. The format:"binary" tag indicates a file upload field.

File Validation

Validate file uploads before processing.

func uploadHandler(ctx forge.Context) error {
    file, header, err := ctx.FormFile("document")
    if err != nil {
        return forge.BadRequest("file required")
    }
    defer file.Close()

    // Validate file size (10MB max)
    maxSize := int64(10 * 1024 * 1024)
    if header.Size > maxSize {
        return forge.BadRequest(fmt.Sprintf(
            "file too large: %d bytes (max %d bytes)",
            header.Size, maxSize,
        ))
    }

    // Validate content type
    allowedTypes := map[string]bool{
        "application/pdf":  true,
        "image/jpeg":       true,
        "image/png":        true,
    }

    contentType := header.Header.Get("Content-Type")
    if !allowedTypes[contentType] {
        return forge.BadRequest(fmt.Sprintf(
            "unsupported file type: %s", contentType,
        ))
    }

    // Validate file extension
    ext := filepath.Ext(header.Filename)
    allowedExts := map[string]bool{
        ".pdf": true, ".jpg": true, ".jpeg": true, ".png": true,
    }
    if !allowedExts[ext] {
        return forge.BadRequest("unsupported file extension: " + ext)
    }

    // Process the validated file
    data, err := io.ReadAll(file)
    if err != nil {
        return forge.InternalError(err)
    }

    return ctx.JSON(201, processDocument(header.Filename, data))
}

Size Limits

Set maximum request body size to prevent memory exhaustion from large uploads. This is typically done at the server or middleware level.

func maxBodySize(maxBytes int64) forge.Middleware {
    return func(next forge.Handler) forge.Handler {
        return func(ctx forge.Context) error {
            ctx.Request().Body = http.MaxBytesReader(
                ctx.Response(), ctx.Request().Body, maxBytes,
            )
            return next(ctx)
        }
    }
}

// Apply to upload routes
r.POST("/upload", uploadHandler,
    forge.WithMiddleware(maxBodySize(50 * 1024 * 1024)), // 50MB
)

OpenAPI File Upload Response

Use WithFileUploadResponse to generate a standard upload success schema.

r.POST("/upload", uploadHandler,
    forge.WithRequestContentTypes("multipart/form-data"),
    forge.WithRequestSchema(&UploadRequest{}),
    forge.WithFileUploadResponse(201),
    forge.WithErrorResponses(),
    forge.WithTags("uploads"),
)

The generated response schema includes:

{
  "fileId": "string",
  "filename": "string",
  "size": 0,
  "contentType": "string",
  "url": "string"
}

Complete Example

package main

import (
    "fmt"
    "io"
    "path/filepath"

    "github.com/xraph/forge"
)

type UploadRequest struct {
    Title    string `form:"title" description:"File title"`
    Category string `form:"category" description:"File category" enum:"image,document,video"`
    File     []byte `form:"file" format:"binary" description:"The file to upload"`
}

type UploadResponse struct {
    ID       string `json:"id"`
    Filename string `json:"filename"`
    Size     int64  `json:"size"`
    URL      string `json:"url"`
}

func main() {
    app := forge.New(forge.WithAppName("upload-service"))
    r := app.Router()

    r.POST("/api/files", func(ctx forge.Context) error {
        // Bind form fields
        title := ctx.Query("title")

        // Get the file
        file, header, err := ctx.FormFile("file")
        if err != nil {
            return forge.BadRequest("file is required")
        }
        defer file.Close()

        // Validate
        if header.Size > 25*1024*1024 {
            return forge.BadRequest("file exceeds 25MB limit")
        }

        // Read and store
        data, err := io.ReadAll(file)
        if err != nil {
            return forge.InternalError(err)
        }

        ext := filepath.Ext(header.Filename)
        fileID := generateID()

        return ctx.JSON(201, UploadResponse{
            ID:       fileID,
            Filename: header.Filename,
            Size:     header.Size,
            URL:      fmt.Sprintf("/files/%s%s", fileID, ext),
        })
    },
        forge.WithSummary("Upload a file"),
        forge.WithTags("files"),
        forge.WithRequestSchema(&UploadRequest{}),
        forge.WithRequestContentTypes("multipart/form-data"),
        forge.WithCreatedResponse(&UploadResponse{}),
        forge.WithErrorResponses(),
    )

    app.Run()
}

How is this guide?

On this page