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
| Concept | Flask / Python | GoFr / Go |
|---|---|---|
| Concurrency | WSGI process / thread workers | Goroutines (one process, true concurrency) |
| Route | @app.route('/users/<id>') | app.GET("/users/{id}", handler) |
| Request body | request.get_json() | c.Bind(&struct) |
| Path param | def view(id): (function arg) | c.PathParam("id") |
| Query param | request.args.get('q') | c.Param("q") |
| Response | return jsonify(data), 200 | return data, nil |
| Error response | abort(404) | return nil, fmt.Errorf("not found") |
| Logging | logging + structlog | Built-in GoFr structured logging |
| Tracing | OpenTelemetry Python instrumentation | Built into GoFr |
| Database | SQLAlchemy / psycopg / pymongo | Built-in clients |
| Background jobs | Celery / RQ | GoFr cron, Pub/Sub subscribers |
Hello world
Flask:
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:
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*Contextparameter; pass it where you need it. Goroutines + a goroutine-localrequestdon'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 withsqlcfor type-safe queries if you want ORM-like ergonomics. - Decorators don't translate.
@app.before_requestbecomes middleware;@app.errorhandlerbecomes explicit error mapping in your handlers. abort(404)becomesreturn nil, errwhereerris a typed error that implementsStatusCode() int. A plainerrorserializes as a 500. Return one of GoFr's built-in error types (http.ErrorEntityNotFound, etc.) or define your own type satisfying theStatusCode()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.
Recommended adoption
- Pick a small Flask service (an internal webhook, a CRUD API) and rebuild it in GoFr.
- Run side-by-side, validate observability output.
- Iterate — port more services as your team gains comfort.

