Protobuf + ConnectRPC definitions for Users and Posts services β with generated TypeScript and Go code, field-level validation via protovalidate, and automatic NPM publishing via GitHub Actions.
| Service | RPCs |
|---|---|
UserService |
CreateUser Β· GetUser Β· UpdateUser Β· DeleteUser Β· ListUsers |
PostService |
CreatePost Β· GetPost Β· UpdatePost Β· DeletePost Β· ListPosts |
All request fields carry protovalidate constraints encoded directly in the .proto files:
| Field | Rule |
|---|---|
id fields |
UUID v4 format |
name |
2β100 chars Β· alphanumeric, spaces, _, - |
email |
RFC 5322 email format |
role / status |
Must be a defined enum value (UNSPECIFIED disallowed on create) |
page |
>= 1 |
page_size |
1 β 100 |
tags |
Max 20 items Β· each 1β50 chars |
content |
Max 100,000 chars |
| Update/filter fields | IGNORE_IF_ZERO_VALUE β constraint skipped when field is empty/zero |
proto-package-example/
βββ protos/
β βββ users/v1/
β β βββ users.proto # package users.v1 β UserService + validation
β βββ posts/v1/
β βββ posts.proto # package posts.v1 β PostService + validation
β
βββ gen/ # β οΈ Generated β do not edit manually
β βββ ts/ # TypeScript source (compiled β dist/)
β β βββ users/v1/
β β β βββ users_pb.ts # User messages & enums
β β β βββ users_connect.ts # UserService descriptor
β β βββ posts/v1/
β β β βββ posts_pb.ts # Post messages & enums
β β β βββ posts_connect.ts # PostService descriptor
β β βββ buf/validate/
β β βββ validate_pb.ts # protovalidate runtime types
β βββ go/ # Go packages (go get-able)
β βββ users/v1/
β β βββ users.pb.go # package usersv1 β proto messages
β β βββ usersv1connect/
β β βββ users.connect.go # package usersv1connect β service interface & client
β βββ posts/v1/
β β βββ posts.pb.go # package postsv1 β proto messages
β β βββ postsv1connect/
β β βββ posts.connect.go # package postsv1connect β service interface & client
β βββ buf/validate/
β βββ validate.pb.go # protovalidate Go types
β
βββ dist/ # Built JS + .d.ts (published to npm)
β βββ users/v1/
β β βββ users_pb.js / .d.ts
β β βββ users_connect.js / .d.ts
β βββ posts/v1/
β βββ posts_pb.js / .d.ts
β βββ posts_connect.js / .d.ts
β
βββ buf.yaml # Buf workspace config (lint: STANDARD)
βββ buf.gen.yaml # Code generation plugins
βββ buf.lock # Locked buf dependency versions
βββ go.mod # Go module (github.com/sa3akash/proto-package-example)
βββ tsconfig.json # Compiles gen/ts β dist
βββ package.json # NPM package (@sa3akash/proto)
Why
usersv1connect/andpostsv1connect/?protoc-gen-connect-gointentionally generates the ConnectRPC service interface, handler, and client into a separate Go package (usersv1connect) from the proto messages (usersv1). This prevents circular imports β your server implementations import messages fromusersv1and the service contract fromusersv1connect.
npm install @sa3akash/proto @bufbuild/protobuf @connectrpc/connect| Import | Contents |
|---|---|
@sa3akash/proto/users |
User, UserRole, all request/response types |
@sa3akash/proto/users/connect |
UserService ConnectRPC descriptor |
@sa3akash/proto/posts |
Post, PostStatus, all request/response types |
@sa3akash/proto/posts/connect |
PostService ConnectRPC descriptor |
import { User, UserRole } from "@sa3akash/proto/users";
import { Post, PostStatus } from "@sa3akash/proto/posts";
const user: User = {
id: "550e8400-e29b-41d4-a716-446655440000",
name: "Jane Doe",
email: "jane@example.com",
role: UserRole.USER,
};import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";
import { UserService } from "@sa3akash/proto/users/connect";
import type { CreateUserRequest } from "@sa3akash/proto/users";
import { UserRole } from "@sa3akash/proto/users";
const transport = createConnectTransport({
baseUrl: "https://api.example.com",
httpVersion: "2",
});
const client = createClient(UserService, transport);
const res = await client.createUser({
name: "Jane Doe",
email: "jane@example.com",
role: UserRole.USER,
});
console.log(res.user);import { ConnectRouter } from "@connectrpc/connect";
import { UserService } from "@sa3akash/proto/users/connect";
import type { CreateUserRequest, CreateUserResponse } from "@sa3akash/proto/users";
export function registerRoutes(router: ConnectRouter) {
router.service(UserService, {
async createUser(req): Promise<CreateUserResponse> {
return {
user: {
id: crypto.randomUUID(),
name: req.msg.name,
email: req.msg.email,
role: req.msg.role,
},
};
},
});
}npm install @bufbuild/protovalidateimport { createValidator } from "@bufbuild/protovalidate";
import { CreateUserRequest } from "@sa3akash/proto/users";
const validator = await createValidator();
try {
validator.validate(new CreateUserRequest({ name: "", email: "bad" }));
} catch (err) {
console.error(err.violations); // field-level violation details
}go get github.com/sa3akash/proto-package-example| Import path | Package | Contents |
|---|---|---|
.../gen/go/users/v1 |
usersv1 |
Proto messages: User, CreateUserRequest, β¦ |
.../gen/go/users/v1/usersv1connect |
usersv1connect |
UserServiceClient, UserServiceHandler, NewUserServiceHandler |
.../gen/go/posts/v1 |
postsv1 |
Proto messages: Post, CreatePostRequest, β¦ |
.../gen/go/posts/v1/postsv1connect |
postsv1connect |
PostServiceClient, PostServiceHandler, NewPostServiceHandler |
package main
import (
"context"
"net/http"
"connectrpc.com/connect"
"buf.build/go/protovalidate"
proto "github.com/sa3akash/proto-package-example/gen/go"
"github.com/sa3akash/proto-package-example/gen/go/usersv1connect"
)
type UserServer struct{}
func (s *UserServer) CreateUser(
ctx context.Context,
req *connect.Request[proto.CreateUserRequest],
) (*connect.Response[proto.CreateUserResponse], error) {
// Validate request fields against protovalidate constraints
v, _ := protovalidate.New()
if err := v.Validate(req.Msg); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
user := &proto.User{
Id: "generated-uuid",
Name: req.Msg.Name,
Email: req.Msg.Email,
Role: req.Msg.Role,
}
return connect.NewResponse(&proto.CreateUserResponse{User: user}), nil
}
func main() {
mux := http.NewServeMux()
path, handler := usersv1connect.NewUserServiceHandler(&UserServer{})
mux.Handle(path, handler)
http.ListenAndServe(":8080", mux)
}import (
"net/http"
"connectrpc.com/connect"
proto "github.com/sa3akash/proto-package-example/gen/go/users/v1"
"github.com/sa3akash/proto-package-example/gen/go/users/v1/usersv1connect"
)
client := usersv1connect.NewUserServiceClient(
http.DefaultClient,
"https://api.example.com",
)
resp, err := client.GetUser(ctx, connect.NewRequest(&proto.GetUserRequest{
Id: "550e8400-e29b-41d4-a716-446655440000",
}))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Msg.User)# First time β fetch buf dependencies
bun run generate # runs: buf generate
# or directly:
bunx buf dep update
bunx buf generateGenerated files appear in gen/ts/ and gen/go/.
bun install
bun run build # runs: tsc -p tsconfig.jsongo mod tidy
go build ./...Push a version tag β GitHub Actions handles the rest:
git tag v1.0.1
git push origin v1.0.1The publish.yml workflow will:
- Run
buf generate(fresh TS + Go code) - Build TypeScript (
tsc) - Stamp version from the git tag
- Publish
@sa3akash/proto@1.0.1to npmjs.com
| Secret | Description |
|---|---|
NPM_TOKEN |
npm automation token with publish access |
Create at: https://www.npmjs.com/settings/tokens