Skip to main content

Migrate from Flask (Python) to GoFr

Summary

Flask developers tend to like GoFr because both are minimal in the right places — small, opinionated cores with sensible defaults. Flask's @app.route decorator becomes app.GET("/path", handler). Request access via request.json becomes c.Bind(&struct).

Migrating with an AI assistant?

Hand https://gofr.dev/AGENTS.md to your coding assistant (Claude Code, Cursor, Codex, Aider). It contains the framework conventions, routing/binding/datasource patterns, and per-framework cheat-sheets so the assistant can translate handlers without you re-explaining GoFr.

Mental model translation

ConceptFlask / PythonGoFr / Go
ConcurrencyWSGI process / thread workersGoroutines (one process, true concurrency)
Route@app.route('/users/<id>')app.GET("/users/{id}", handler)
Request bodyrequest.get_json()c.Bind(&struct)
Path paramdef view(id): (function arg)c.PathParam("id")
Query paramrequest.args.get('q')c.Param("q")
Responsereturn jsonify(data), 200return data, nil
Error responseabort(404)return nil, fmt.Errorf("not found")
Logginglogging + structlogBuilt-in GoFr structured logging
TracingOpenTelemetry Python instrumentationBuilt into GoFr
DatabaseSQLAlchemy / psycopg / pymongoBuilt-in clients
Background jobsCelery / RQGoFr cron, Pub/Sub subscribers

Hello world

Flask:

python
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/hello')
def hello():
    return jsonify(message="Hello, world")

if __name__ == '__main__':
    app.run(port=8000)

GoFr:

Go
package main

import "gofr.dev/pkg/gofr"

func main() {
    app := gofr.New()
    app.GET("/hello", func(c *gofr.Context) (any, error) {
        return "Hello, world", nil
    })
    app.Run()
}

Concurrency: from gunicorn workers to goroutines

A typical Flask deployment runs gunicorn with N worker processes (or threads). Each request occupies one worker for its duration, including I/O wait. Scaling is by adding processes / replicas.

A GoFr service is a single binary. Each request is a goroutine. I/O is non-blocking. You typically need fewer instances at the same throughput.

What you can drop

  • python-json-logger / structlog config → built-in.
  • flask-prometheus-metrics → built-in.
  • opentelemetry-instrumentation-flask → built-in.
  • Custom DB connection pooling on top of SQLAlchemy → handled by GoFr's SQL client.
  • flask-healthz / hand-rolled /healthz → auto-exposed at /.well-known/health.

Common gotchas

  • No global request. The handler receives a *Context parameter; pass it where you need it. Goroutines + a goroutine-local request don't mix in Go.
  • @app.errorhandler(Exception) becomes explicit error returns. Every error travels back as the second return value.
  • Database sessions aren't flask-sqlalchemy. GoFr's SQL client gives you a connection pool with raw queries; pair with sqlc for type-safe queries if you want ORM-like ergonomics.
  • Decorators don't translate. @app.before_request becomes middleware; @app.errorhandler becomes explicit error mapping in your handlers.
  • abort(404) becomes return nil, err where err is a typed error that implements StatusCode() int. A plain error serializes as a 500. Return one of GoFr's built-in error types (http.ErrorEntityNotFound, etc.) or define your own type satisfying the StatusCode() interface so the responder picks up the right HTTP code. See Error Handling.

Estimated effort per service

A small Flask service (10-20 routes) typically takes 2–4 engineering days for a Python developer new to Go. Most of the time is spent on Go idioms.

  1. Pick a small Flask service (an internal webhook, a CRUD API) and rebuild it in GoFr.
  2. Run side-by-side, validate observability output.
  3. Iterate — port more services as your team gains comfort.

Frequently asked