Laravel + Vue (Inertia) + gRPC: building a simple BFF that talks to a gRPC User service

Published: (January 3, 2026 at 03:49 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Why gRPC in a Laravel + Vue project?

If you’re building a modern Laravel + Vue app, your default instinct is usually REST/JSON.
gRPC is different: you define your API as a contract in a .proto file, then generate strongly‑typed client/server code (stubs). It runs over HTTP/2 and uses Protocol Buffers for compact serialization.

Note: A Vue app in the browser can’t talk gRPC directly without extra infrastructure (gRPC‑Web + a proxy such as Envoy).
A practical pattern is:

✅ Vue (browser) → Laravel (HTTP/JSON) → gRPC microservice (internal)

Laravel becomes your BFF (Backend‑For‑Frontend). This repository demonstrates exactly that.

Goal

Display a user on a Vue page, but fetch it through Laravel, which calls a gRPC server.

Flow

  1. Vue page (UserShow.vue) calls GET /api/users/{id}.
  2. Laravel API route invokes App\Services\UserGrpc.
  3. UserGrpc uses the generated gRPC client to call UserService.GetUser.
  4. A Node gRPC server returns user data (demo reads the same SQLite DB Laravel uses).

Folder layout (highlights)

proto/user/v1/user.proto          → contract
generated/App/Grpc/...           → generated PHP stubs (client classes)
grpc-server/                     → demo gRPC server (Node)
app/Services/UserGrpc.php         → Laravel gRPC client wrapper
resources/js/Pages/UserShow.vue  → Vue / Inertia page

For a fresh project, Laravel’s Vue starter kit (Inertia) is a great base.

1️⃣ Define the contract (proto/user/v1/user.proto)

syntax = "proto3";

package user.v1;

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  string id   = 1;
  string name = 2;
}

The .proto file is the single source of truth.

2️⃣ Generate PHP client stubs

buf standardises Protobuf workflows and generation config (buf.yaml, buf.gen.yaml).

npx buf generate

Generated files live under generated/App/Grpc/....

Tip: Many teams do not commit generated code (they generate it in CI).
Committing it is fine for demos and fast setup.

Important: Generating PHP client stubs does not create a server. You still need a gRPC server implementation (Go, Node, Java, PHP, …). In this repo the demo server is under grpc-server/.

3️⃣ Demo gRPC server (Node)

cd grpc-server
npm install

If you see errors like Cannot find module 'dotenv' or Cannot find module 'better-sqlite3', install the missing packages:

npm install dotenv better-sqlite3 @grpc/grpc-js @grpc/proto-loader

grpc-server/server.js

require("dotenv").config();
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = __dirname + "/../proto/user/v1/user.proto";
const packageDef = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const userProto = grpc.loadPackageDefinition(packageDef).user.v1;

// Demo handler
function GetUser(call, callback) {
  const { id } = call.request;

  // Minimal demo response
  callback(null, { id, name: `User #${id}` });
}

function main() {
  const server = new grpc.Server();
  server.addService(userProto.UserService.service, { GetUser });

  const addr = process.env.GRPC_ADDR || "0.0.0.0:50051";
  server.bindAsync(
    addr,
    grpc.ServerCredentials.createInsecure(),
    () => {
      console.log(`gRPC server listening on ${addr}`);
      server.start();
    }
  );
}

main();

Start the server:

node server.js

If Laravel reports Connection refused, the gRPC server isn’t running or isn’t listening on the expected address/port.

4️⃣ Laravel gRPC client wrapper (app/Services/UserGrpc.php)

client = new UserServiceClient(
            env('USER_SVC_ADDR', '127.0.0.1:50051'),
            ['credentials' => ChannelCredentials::createInsecure()]
        );
    }

    public function getUser(string $id): array
    {
        $req = new GetUserRequest();
        $req->setId($id);

        /** @var \App\Grpc\User\V1\GetUserResponse $resp */
        [$resp, $status] = $this->client->GetUser($req)->wait();

        if ($status->code !== STATUS_OK) {
            throw new \RuntimeException($status->details, $status->code);
        }

        return [
            'id'   => $resp->getId(),
            'name' => $resp->getName(),
        ];
    }
}

Common issue: Grpc\ChannelCredentials not found

That usually means the gRPC PHP extension isn’t enabled for the PHP you’re running (CLI vs FPM can differ).

php -m | grep grpc   # should list "grpc"
php --ini            # shows which php.ini is loaded

Enable the extension in the correct php.ini:

extension=grpc.so

5️⃣ Expose a classic JSON API endpoint

routes/api.php

use Illuminate\Support\Facades\Route;
use App\Services\UserGrpc;

Route::get('/users/{id}', function (string $id, UserGrpc $userGrpc) {
    return response()->json($userGrpc->getUser($id));
});

routes/web.php (Inertia page route)

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/users/{id}', function (string $id) {
    return Inertia::render('UserShow', [
        'id' => $id,
    ]);
});

6️⃣ Vue page (resources/js/Pages/UserShow.vue)

A clean Inertia‑friendly version uses props (no Vue Router required).

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";

type User = { id: string; name: string };

const props = defineProps<{ id: string }>();
const id = computed(() => props.id);

const loading = ref(true);
const error = ref<string | null>(null);
const user = ref<User | null>(null);

onMounted(async () => {
  try {
    const resp = await fetch(`/api/users/${id.value}`);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    user.value = await resp.json();
  } catch (e: any) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <div>
    <h2>User {{ id }}</h2>

    <div v-if="loading">Loading…</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <p><strong>ID:</strong> {{ user?.id }}</p>
      <p><strong>Name:</strong> {{ user?.name }}</p>
    </div>
  </div>
</template>

Recap

StepWhat you do
1Write a .proto contract.
2Generate PHP client stubs (buf generate).
3Run a gRPC server (Node demo).
4Wrap the generated client in a Laravel service (UserGrpc).
5Expose a JSON API endpoint that calls the service.
6Inertia/Vue page fetches the JSON endpoint.

With this setup you get:

  • Strong typing across services (thanks to Protobuf).
  • Efficient binary transport inside your backend (HTTP/2 + protobuf).
  • A clean BFF that shields the browser from gRPC‑Web complexities.

Happy coding! 🚀

Additional Vue example (alternative implementation)

<script setup lang="ts">
import { ref, onMounted } from "vue";

const id = ref(/* route param or similar */);
const user = ref<{ id: string; name: string }>({ id: "", name: "" });
const loading = ref(true);
const error = ref<string | null>(null);

onMounted(async () => {
  try {
    const res = await fetch(`/api/users/${encodeURIComponent(id.value)}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    user.value = await res.json();
  } catch (e: any) {
    error.value = e?.message ?? "Unknown error";
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <div>
    <h2>User</h2>

    <div v-if="loading">Loading…</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <p><strong>ID:</strong> {{ user.id }}</p>
      <p><strong>Name:</strong> {{ user.name }}</p>
    </div>
  </div>
</template>

Running the services

Terminal 1 — gRPC server

cd grpc-server
node server.js

Terminal 2 — Laravel + Vite

composer install
cp .env.example .env
php artisan key:generate

npm install
npm run dev

php artisan serve

When to use this setup

  • You want typed contracts between internal services.
  • You have multiple backends in different languages.
  • You need a stable interface (proto) shared across teams.
  • You want performance and streaming features (gRPC supports streaming).

If your app is a small monolith and the public‑facing API is your only concern, REST might still be simpler.

Back to Blog

Related posts

Read more »