DEV Community

A0mineTV
A0mineTV

Posted on

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

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.

But there’s a catch for browser apps: a Vue app in the browser can’t just “talk gRPC” directly without extra infrastructure (gRPC-Web + a proxy such as Envoy). So a practical pattern is:

Vue (browser) -> Laravel (HTTP/JSON) -> gRPC microservice (internal)

That Laravel layer becomes your BFF (Backend For Frontend).

This repo demonstrates exactly that.


Architecture (what we built)

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 calls App\Services\UserGrpc
  3. UserGrpc uses the generated gRPC client to call UserService.GetUser
  4. A Node gRPC server returns user data (for demo purposes, reading the same SQLite DB Laravel uses)

Folder layout highlights:

  • proto/user/v1/user.proto → the 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

Step 1 — Scaffold Laravel + Vue (Inertia)

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


Step 2 — Define the gRPC contract (.proto)

Create:

proto/user/v1/user.proto

Example contract:

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;
}
Enter fullscreen mode Exit fullscreen mode

This .proto file is the single source of truth.


Step 3 — Generate PHP gRPC client code

Once you have the .proto, you generate client classes for PHP.

Using Buf for generation

Buf is a tool that standardizes Protobuf workflows and generation config (buf.yaml, buf.gen.yaml).

npx buf generate
Enter fullscreen mode Exit fullscreen mode

This repo keeps generated files under:

generated/App/Grpc/...

Tip: Many teams do NOT commit generated code (they generate in CI). But committing it is OK for demos and fast setup.


Step 4 — Implement a gRPC Server (Node demo)

Important: generating PHP client stubs does NOT create a server.

You still need a gRPC server implementation in some language (Go, Node, Java, PHP, etc).

In this repo, the demo server is under grpc-server/ and is started with Node.

Install deps:

cd grpc-server
npm install
Enter fullscreen mode Exit fullscreen mode

If you see errors like:

  • Cannot find module 'dotenv'
  • Cannot find module 'better-sqlite3'

That just means you haven’t installed the packages yet:

npm install dotenv better-sqlite3 @grpc/grpc-js @grpc/proto-loader
Enter fullscreen mode Exit fullscreen mode

A minimal server.js shape looks like:

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;

  // For a minimal demo:
  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();
Enter fullscreen mode Exit fullscreen mode

Start the gRPC server:

node server.js
Enter fullscreen mode Exit fullscreen mode

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


Step 5 — Laravel gRPC client wrapper (UserGrpc)

In Laravel, we wrap generated gRPC calls in a service class:

app/Services/UserGrpc.php

Important fix: return values must come from $resp, not from $req.

<?php

declare(strict_types=1);

namespace App\Services;

use App\Grpc\User\V1\GetUserRequest;
use App\Grpc\User\V1\UserServiceClient;
use Grpc\ChannelCredentials;
use const Grpc\STATUS_OK;

class UserGrpc
{
    private UserServiceClient $client;

    public function __construct()
    {
        $this->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);

        [$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(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

If Grpc\ChannelCredentials is “not found”

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

Quick checks:

php -m | grep grpc
php --ini
Enter fullscreen mode Exit fullscreen mode

And enable extension=grpc.so in the correct php.ini (the one shown by php --ini).


Step 6 — Expose an API endpoint in Laravel

Because the browser shouldn’t call gRPC directly, we 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));
});
Enter fullscreen mode Exit fullscreen mode

Step 7 — Render a Vue page with Inertia

We keep your normal Inertia “page route” in routes/web.php:

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

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

Then your Vue page (resources/js/Pages/UserShow.vue) can fetch from /api/users/{id}.

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>({ id: "", name: "" });

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 class="p-4">
    <h1 class="text-xl font-semibold">User</h1>

    <p v-if="loading">Loading...</p>
    <p v-else-if="error" class="text-red-600">{{ error }}</p>

    <div v-else class="mt-3">
      <div><b>ID:</b> {{ user.id }}</div>
      <div><b>Name:</b> {{ user.name }}</div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Now you can open:

  • http://127.0.0.1:8000/users/1

…and it will load user data through the full chain:
Vue → Laravel API → gRPC client → gRPC server.


Step 8 — Seed fake users

Since you already have Laravel’s DB, the easiest approach is:

  1. Create a seeder (or factory)
  2. Run migrate + seed

Example:

php artisan migrate --seed
Enter fullscreen mode Exit fullscreen mode

If your gRPC server reads the same SQLite DB file, it can return real seeded rows.


Run everything (local)

Terminal 1 — gRPC server:

cd grpc-server
node server.js
Enter fullscreen mode Exit fullscreen mode

Terminal 2 — Laravel + Vite:

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

npm install
npm run dev

php artisan serve
Enter fullscreen mode Exit fullscreen mode

When is this pattern worth it?

Use this setup when:

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

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

Source: https://github.com/VincentCapek/laravel-vue-grpc-bff

Top comments (0)