# Running Python, Go, Rust, and Ruby backends alongside a Next.js frontend

- Date: 2026-04-20T14:27:44.262Z
- Tags: Platform
- URL: https://render.com/articles/running-python-go-rust-and-ruby-backends-alongside-a-next-js-frontend


## When this pattern makes sense

Sometimes the frontend and backend want different deployment environments. You might keep Next.js on Vercel, but run your backend in a language that better fits the rest of your system. That is common when your API depends on Python libraries, Go concurrency, Rust performance, or Rails conventions.

This setup also gives you a clear operational split. Vercel serves the frontend. Render runs the backend as a web service with its own health checks, environment variables, deploy lifecycle, and optional companion services such as [background workers](https://render.com/docs/background-workers) or [private services](https://render.com/docs/private-services).

This article focuses on the integration boundary between those two platforms. The pattern stays the same regardless of language:

- Serve the Next.js frontend from Vercel
- Expose a JSON API from Render
- Pass the backend URL to the frontend with `NEXT_PUBLIC_API_URL`
- Allow the frontend origin with CORS
- Deploy the backend from a shared repo with Render Blueprints

## Organize the repository

A monorepo is a practical default for this pattern. You can update the API and the frontend in one pull request, while still deploying them independently.

Use a layout similar to this:

```text
my-app/
├── frontend/              # Next.js application deployed to Vercel
│   ├── package.json
│   ├── next.config.js
│   └── src/
├── backend/               # Python, Go, Rust, or Ruby service deployed to Render
│   ├── ...
│   └── ...
├── render.yaml            # Render Blueprints configuration
└── README.md
```

In this arrangement, Vercel builds the `frontend` directory, and Render reads `render.yaml` from the repo root. In the Blueprint, `rootDir` points Render to the backend subdirectory you want to build.

## Build the backend API

Regardless of language, the backend has the same job:

- Bind to `0.0.0.0`
- Read the port from `PORT`
- Expose a health endpoint
- Return JSON from the application endpoint
- Allow the Vercel origin with CORS

> *Use the snippets in this article as illustrations of the integration pattern.*
>
> They show the shape of a backend that works well with Next.js on Vercel and Render on the backend side. They are not complete production templates, and you should adapt dependency setup, auth, error handling, and CORS details to your framework and app.

### Python with FastAPI

[FastAPI](https://fastapi.tiangolo.com/) works well when you want an async Python API with minimal boilerplate. This example is intentionally minimal and focuses on the integration points.

```python
# backend/main.py
import os

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=os.environ.get("ALLOWED_ORIGINS", "http://localhost:3000").split(","),
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
)

@app.get("/api/health")
async def health():
    return {"status": "ok", "service": "python-backend"}
```

On Render, you typically start this service with `uvicorn main:app --host 0.0.0.0 --port $PORT`.

### Go with `net/http`

Go's standard library is enough for a small API service. This example is intentionally minimal. The key details are handling the CORS preflight request and listening on `PORT`.

```go
// backend/main.go
package main

import (
	"log"
	"net/http"
	"os"
	"strings"
)

func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
	allowed := os.Getenv("ALLOWED_ORIGINS")
	if allowed == "" {
		allowed = "http://localhost:3000"
	}

	return func(w http.ResponseWriter, r *http.Request) {
		origin := r.Header.Get("Origin")
		for _, candidate := range strings.Split(allowed, ",") {
			if strings.TrimSpace(candidate) == origin {
				w.Header().Set("Access-Control-Allow-Origin", origin)
				w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
				w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
				break
			}
		}

		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}

		next(w, r)
	}
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "10000"
	}

	http.HandleFunc("/api/health", corsMiddleware(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"status":"ok","service":"go-backend"}`))
	}))

	log.Fatal(http.ListenAndServe("0.0.0.0:"+port, nil))
}
```

Render sets `PORT` for every [web service](https://render.com/docs/web-services), so the fallback exists only for local development.

### Rust with Axum

Rust teams often want the same separation: Next.js on one side, a typed API on the other. This example uses [Axum](https://github.com/tokio-rs/axum) and follows the same CORS and health check pattern as the other runtimes. Treat it as a sketch of the service shape, not a complete Axum setup.

```rust pseudocode
// backend/src/main.rs
use axum::{routing::get, Json, Router};
use serde_json::json;
use std::env;
use tower_http::cors::CorsLayer;

async fn health() -> Json<serde_json::Value> {
    Json(json!({ "status": "ok", "service": "rust-backend" }))
}

#[tokio::main]
async fn main() {
    let _allowed_origins = env::var("ALLOWED_ORIGINS").unwrap_or_else(|_| "http://localhost:3000".to_string());
    let _port = env::var("PORT").unwrap_or_else(|_| "10000".to_string());

    let app = Router::new()
        .route("/api/health", get(health))
        .layer(CorsLayer::new());

    // Bind to 0.0.0.0 and convert ALLOWED_ORIGINS into your Axum/Tower CORS config.
    // The exact code depends on the crates and versions you use.
}
```

Render supports `runtime: rust` in `render.yaml`, so the deployment shape matches the Python, Go, and Ruby examples.

### Ruby with Rails API mode

If your backend already lives in Rails, use API mode and keep the same public contract: one health endpoint, one application endpoint, and CORS driven by environment variables. This snippet shows the shape of the integration, not a full Rails app.

```ruby pseudocode
# backend/config/routes.rb
namespace :api do
  get "health", to: "health#show"
end

# backend/app/controllers/api/health_controller.rb
module Api
  class HealthController < ApplicationController
    def show
      render json: { status: "ok", service: "ruby-backend" }
    end
  end
end

# backend/config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ENV.fetch("ALLOWED_ORIGINS", "http://localhost:3000").split(",")
    resource "/api/*", headers: :any, methods: [:get, :post, :options]
  end
end
```

On Render, Rails typically runs with a `bundle exec` start command that binds to `0.0.0.0` and reads `PORT`.

## Connect Next.js to the backend

On the frontend, the integration point is the backend base URL. In Next.js, environment variables prefixed with `NEXT_PUBLIC_` are bundled for the browser, so `NEXT_PUBLIC_API_URL` is a good fit.

```javascript pseudocode
// frontend/src/app/dashboard/page.js
const API_URL = process.env.NEXT_PUBLIC_API_URL;

export default async function DashboardPage() {
  const res = await fetch(`${API_URL}/api/data`, {
    headers: { "Content-Type": "application/json" },
    cache: "no-store",
  });

  if (!res.ok) {
    return <p>Failed to load data from backend.</p>;
  }

  const data = await res.json();

  return (
    <main>
      <h1>Dashboard</h1>
      <ul>
        {data.items.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </main>
  );
}
```

Set `NEXT_PUBLIC_API_URL` to the local URL your backend uses during development (for example, `http://localhost:10000`) and to your Render service URL in Vercel for production. Because Next.js inlines `NEXT_PUBLIC_` values at build time, redeploy the frontend after you change it.

## Deploy the backend with Render Blueprints

[Render Blueprints](https://render.com/docs/infrastructure-as-code) let you define the backend service in `render.yaml` and keep the deployment config in the same repo as the app.

Here is an illustrative Blueprint for the FastAPI example:

```yaml
# render.yaml
services:
  - type: web
    name: my-python-backend
    runtime: python
    repo: https://github.com/your-org/my-app
    rootDir: backend
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
    healthCheckPath: /api/health
    envVars:
      - key: ALLOWED_ORIGINS
        value: https://my-app.vercel.app
```

The same structure works for the other runtimes:

- Use `runtime: go` for Go
- Use `runtime: rust` for Rust
- Use `runtime: ruby` for Rails
- Keep `rootDir` pointed at the backend directory
- Set `ALLOWED_ORIGINS` to the exact Vercel origin you want to allow

When you sync the Blueprint in [the Render Dashboard](https://dashboard.render.com/), Render creates the defined service and redeploys it when you push changes to the linked branch.

## Handle the operational details

This pattern stays manageable when you keep a few operational rules consistent.

### Keep CORS explicit

Set `ALLOWED_ORIGINS` to the exact frontend origin, including `https://`. Avoid `*` for authenticated APIs, and make sure your backend handles `OPTIONS` requests for browser preflights.

### Treat health checks as part of the contract

Give the service a lightweight endpoint such as `/api/health`, then set `healthCheckPath` in the Blueprint or in the Render Dashboard. Render uses this endpoint during [zero-downtime deploys](https://render.com/deploys#zero-downtime-deploys) and for ongoing health checks.

### Keep public and private configuration separate

Use `NEXT_PUBLIC_API_URL` only for the backend base URL that the browser needs. Keep secrets, tokens, and database credentials in non-public environment variables on Render.

### Add companion services when the backend grows

If the same backend also needs async jobs, add a [background worker](https://render.com/docs/background-workers). If it needs an internal-only API for other Render services, add a [private service](https://render.com/docs/private-services). You do not need to move the frontend off Vercel to use either service type.

## Frequently asked questions

###### Should I keep the frontend and backend in one repo?

You can use one repo or two. A monorepo is often easier because you can version the frontend, backend, and `render.yaml` together. Separate repos can also work, as long as the frontend points to the correct backend URL.

###### Do I need CORS if my frontend is on Vercel and my API is on Render?

Yes. The browser sees those as different origins, so your backend must allow the Vercel origin explicitly. Set `ALLOWED_ORIGINS` to the exact deployment URL you want to permit, and make sure your API responds correctly to `OPTIONS` preflight requests.

###### Can I use Render Postgres with this setup?

Yes. Your backend can connect to [Render Postgres](https://render.com/docs/postgresql-creating-connecting) the same way any other Render service does. Keep the database credentials on the backend only, and pass only the public API URL to the frontend.

###### Should the backend be a web service or a private service?

If the Next.js frontend on Vercel needs to call the backend over the public internet, the backend must be a web service. Use a private service only for internal traffic from other Render services on the same private network.

###### Can I run workers or preview environments for the backend too?

Yes. Add background workers to the same Blueprint when the backend needs queue processing. You can also use [Preview Environments](https://render.com/docs/preview-environments) to create disposable backend infrastructure for pull requests, which pairs well with Vercel preview deployments.

