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:
-
Vue page (
UserShow.vue) callsGET /api/users/{id} -
Laravel API route calls
App\Services\UserGrpc -
UserGrpcuses the generated gRPC client to callUserService.GetUser - 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;
}
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
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
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
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();
Start the gRPC server:
node server.js
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(),
];
}
}
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
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));
});
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,
]);
});
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>
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:
- Create a seeder (or factory)
- Run migrate + seed
Example:
php artisan migrate --seed
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
Terminal 2 — Laravel + Vite:
composer install
cp .env.example .env
php artisan key:generate
npm install
npm run dev
php artisan serve
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)