এই পর্বে যা শিখবেন
- Frontend-backend type-এর ফাঁক থেকে চলার সময় কী কী ভাঙে
- npm workspaces দিয়ে বাজারঘরের monorepo সাজানো
- tRPC-র router, procedure, input, query, mutation, context আর middleware
- Express 5 adapter আর React client জোড়া লাগানো, TanStack Query সহ
- Server-এ নাম বদলে client-এ আসল
tscerror দেখা, আর bundle খুলে server কোড খোঁজা - REST ও OpenAPI, GraphQL আর tRPC-র মধ্যে কারণসহ বাছাই
১ · Frontend আর backend-এর মাঝের ফাঁক
একটি রেস্তোরাঁর কথা ভাবুন। রান্নাঘরে শেফ, সামনে ওয়েটার। ওয়েটারের হাতে ছাপানো মেনু। শেফ একদিন “ভুনা খিচুড়ি”-র নাম বদলে “ঢাকাই খিচুড়ি” করলেন, দামও বদলালেন। মেনু আগের মতোই রইল। ওয়েটার পুরোনো নাম লিখে অর্ডার আনলেন, আর রান্নাঘর বলল, “এই নামে কিছু নেই।” ভুলটি ধরা পড়ল খদ্দেরের সামনে। Frontend হলো ওয়েটার, backend হলো রান্নাঘর, আর API-র type হলো সেই মেনু।
পর্ব ০৬-এ দেখেছেন, TypeScript-এর type চলার সময় থাকে না। পর্ব ২১-এ একটি Zod schema দুই দিকে ব্যবহার করেছেন, আর পর্ব ২৭-এ server-এর দরজায় যাচাই বসিয়েছেন। কিন্তু একটি জায়গা এখনো খোলা। Server যা ফেরত পাঠায়, client সেটিকে কীভাবে চেনে? সাধারণত এভাবে:
type Product = { id: number; name: string; price: number }; // client-এর নিজের নকল
async function getProduct(id: number) {
const res = await fetch(`/api/products/${id}`);
return (await res.json()) as Product; // শুধু একটি প্রতিশ্রুতি
}এখানে as Product কিছুই যাচাই করে না। এটি compiler-কে বলে, “আমাকে বিশ্বাস করুন।” Server বদলে গেলে প্রতিশ্রুতিটি মিথ্যা হয়ে যায়, আর compiler চুপ থাকে। এই অবস্থাকে বলে type drift: দুই পাশের ধারণা ধীরে ধীরে আলাদা হয়ে যাওয়া। ফলাফল এরকম হয়:
| Server-এ বদল | tsc কী বলে | চলার সময় কী ঘটে |
|---|---|---|
price হলো priceTaka | কিছু না | p.price হয় undefined; p.price * 2 হয় NaN |
price সংখ্যা থেকে string "450" | কিছু না | p.price + 20 হয় "45020", মোট দামে ভুল |
createdAt একটি Date | কিছু না | JSON-এ সেটি string, তাই getFullYear() crash করে |
নতুন বাধ্যতামূলক input qty | কিছু না | Server 400 ফেরত দেয়, ব্যবহারকারী দেখেন “কিছু একটা ভুল হয়েছে” |
লক্ষ করুন, ভুলগুলো কোনোটিই বড় নয়। কিন্তু ধরা পড়ে দেরিতে। সবচেয়ে সস্তা জায়গা হলো compile-এর সময়, সবচেয়ে দামি জায়গা হলো ব্যবহারকারীর স্ক্রিন। এই পর্বের লক্ষ্য end-to-end type safety: একটি বদল করলে অন্য প্রান্তের ভাঙা লাইনটি compiler নিজেই দেখিয়ে দেবে।
২ · ফাঁক বন্ধের তিন পথ
রেস্তোরাঁয় ফিরে যাই। মেনুর সমস্যা তিনভাবে মেটানো যায়। শেফ আর ওয়েটার একটি অভিন্ন খাতা দেখে কাজ করতে পারেন। অথবা শেফের মূল তালিকা থেকে ছাপাখানা প্রতিবার নতুন মেনু ছাপতে পারে। অথবা দুজন একই দেয়ালের বোর্ড দেখতে পারেন, যেখানে শেফ লিখলেই ওয়েটার সঙ্গে সঙ্গে দেখেন। সফটওয়্যারের তিন পথও ঠিক এমন।
১. ভাগ করা schema package
একটি package-এ Zod schema থাকে। Server সেটি দিয়ে যাচাই করে, client সেটি থেকে type নেয়। সবচেয়ে সরল পথ। তবে server-এর উত্তর ওই schema মানছে কি না, সেটি আপনাকেই নিশ্চিত করতে হয়।
অভিন্ন খাতা২. Code generation
একটি চুক্তি ফাইল (OpenAPI বা GraphQL schema) থেকে টুল type লেখে। ভাষা-নিরপেক্ষ, তাই Kotlin বা Swift client-ও পায়। দাম হলো একটি বাড়তি build ধাপ।
ছাপাখানা৩. Inference (tRPC)
কোনো আলাদা চুক্তি ফাইল নেই। Client সরাসরি server-এর typeof router পড়ে। কোনো ধাপ নেই, বদল সঙ্গে সঙ্গে দেখা যায়। দাম হলো দুই প্রান্তে TypeScript আর শক্ত বাঁধন।
দ্বিতীয় পথটি একবার চালিয়ে দেখা যাক। চুক্তি লেখা আর openapi-typescript-এর বিস্তার পর্ব ৩৫-এর বিষয়। এখানে শুধু দেখব, নাম বদলালে কখন ধরা পড়ে। Spec-এ Product-এর price আছে, আর client লিখেছে data.price. এবার spec-এ নাম বদলে priceTaka করা হলো।
সত্যিকারের run · openapi-typescript 7.13.0, openapi-fetch 0.17.0, TypeScript 5.9.3 · ~/bazarghar-codegen
$ npx tsc -p . # spec বদলানোর পরে, generate করার আগে
$ echo $?
0
$ npx openapi-typescript openapi.yaml -o schema.ts
✨ openapi-typescript 7.13.0
🚀 openapi.yaml → schema.ts [19.3ms]
$ npx tsc -p .
client.ts(9,32): error TS2339: Property 'price' does not exist on type '{ id: number; name: string; priceTaka: number; }'.
দেখুন, প্রথম tsc কিছুই ধরেনি, কারণ পুরোনো schema.ts তখনো রয়ে গেছে। Code generation-এর ঝুঁকি ঠিক এখানে: generate করতে ভুলে গেলে সুরক্ষা নেই। তাই CI-তে একটি ধাপ রাখা হয়, যা আবার generate করে দেখে ফাইলটি বদলাল কি না। GraphQL-এ একই কাজ করে GraphQL Code Generator-এর মতো টুল (এখানে চালানো হয়নি; GraphQL-এর ভিত্তি পর্ব ৩৬-এ)।
একটি সত্যিকারের ফাঁদ: লেখার সময়ে openapi-typescript 7.13.0-র peer dependency হলো typescript ^5.x. TypeScript 6.0 দিয়ে ইনস্টল করতে গিয়ে আমাদের run-এ ERESOLVE error এসেছে, তাই এই ছোট project-এ 5.9.3 রাখা হয়েছে। একই রকম সীমা আছে typescript-eslint-এ: 8.72-র জন্য typescript লাগে 6.1-এর নিচে, যেমন ~6.0. বাজারঘরের tRPC project-এ এই দুটির কোনোটি নেই, তাই সেখানে 7.0.2 চলেছে। টুল বসানোর আগে তার peer dependency দেখে নিন।
৩ · Monorepo — এক ছাদের নিচে web, api আর shared
পথ যেটিই হোক, type ভাগ করতে হলে কোড এক জায়গায় থাকলে কাজ সহজ হয়। Monorepo মানে একটি repository-র ভেতরে কয়েকটি package. npm-এর workspace সুবিধা এদের পরস্পরের সঙ্গে জুড়ে দেয়, npm-এ প্রকাশ না করেই। পর্ব ০৫-এ ধারণাটি এসেছে, এবার সত্যিই বানাই।
bazarghar/
├── package.json "workspaces": ["packages/*", "apps/*"]
├── tsconfig.base.json strict, verbatimModuleSyntax
├── packages/
│ └── shared/ @bazarghar/shared (Zod schema)
└── apps/
├── api/ @bazarghar/api (Express + tRPC)
└── web/ @bazarghar/web (Vite + React)
import * as z from "zod";
export const productId = z.object({ id: z.number().int().positive() });
export const createOrderInput = z.object({
productId: z.number().int().positive(),
qty: z.number().int().min(1).max(10),
});
export type CreateOrderInput = z.infer<typeof createOrderInput>;Package-এর package.json-এ "exports": { ".": "./src/index.ts" } লেখা। অর্থাৎ অন্য package সরাসরি TypeScript উৎস ফাইল পড়ে, কারণ tsx আর Vite দুটোই সেটি চালাতে পারে। Package বাইরে প্রকাশ করতে চাইলে আগে build করতে হতো। ইনস্টলের পরে যা হলো:
সত্যিকারের run · npm 11.12.1, Node.js 24.15 · ~/bazarghar · লম্বা অংশ ছাঁটা
$ npm install -w @bazarghar/api @trpc/server @trpc/client express superjson @bazarghar/shared@0.0.0
added 69 packages in 2s
$ npm ls --depth=0 --workspaces
+-- @bazarghar/api@0.0.0 -> ./apps/api
| +-- @bazarghar/shared@0.0.0 deduped -> ./packages/shared
| +-- @trpc/client@11.22.1
| +-- @trpc/server@11.22.1
| +-- express@5.3.0
| `-- superjson@2.2.6
+-- @bazarghar/shared@0.0.0 -> ./packages/shared
| `-- zod@4.6.7
`-- @bazarghar/web@0.0.0 -> ./apps/web
+-- @bazarghar/api@0.0.0 deduped -> ./apps/api
+-- @tanstack/react-query@5.105.1
+-- @trpc/tanstack-react-query@11.22.1
+-- react@19.3.1
`-- vite@8.3.5
…
তীরচিহ্ন -> বলছে, এগুলো কপি নয়। node_modules/@bazarghar/shared আসলে packages/shared ফোল্ডারের একটি symlink. তাই shared-এ একটি লাইন বদলালে api আর web সঙ্গে সঙ্গে দেখে, কোনো publish লাগে না। pnpm-এ একই কাজের জন্য থাকে pnpm-workspace.yaml আর "workspace:*"; এই পর্বের run npm দিয়ে হয়েছে।
কোন জিনিস কোন পথে যায়
Types flow at build time and vanish; only JSON crosses at runtime
৪ · tRPC-র গঠন — router থেকে middleware
tRPC-র পুরো নাম TypeScript Remote Procedure Call. RPC মানে অন্য যন্ত্রের একটি function এমনভাবে ডাকা, যেন সেটি নিজের কোডের function. REST-এ আপনি ভাবেন resource আর URL নিয়ে। tRPC-তে ভাবেন function নিয়ে: product.byId, order.create. লেখার সময়ে npm-এর latest হলো 11.22.1; major version 11 এসেছে ২০২৫ সালের মার্চে।
tRPC-র প্রথম প্রমাণ-ধারণা (proof of concept) লিখেছিলেন Zod-এর স্রষ্টা Colin McDonnell. Alex Johansson (KATT) সেটি হাতে নিয়ে প্রকল্পে রূপ দেন। tRPC-র ব্লগে “Introducing tRPC” এসেছে ২০২১ সালের মে মাসে, v10 ২০২২ সালের নভেম্বরে, v11 ২০২৫ সালের মার্চে। এটি TypeScript-এর ২০১৮ সালের 2.8 version-এ আসা conditional type-এর ওপর দাঁড়িয়ে আছে, যা ছাড়া এত গভীর inference সম্ভব হতো না।
প্রথমে ভিত্তি। এই ফাইল একবার লেখা হয়, পুরো backend এটি ব্যবহার করে।
import { initTRPC, TRPCError } from "@trpc/server";
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
import superjson from "superjson";
type User = { id: number; name: string };
// শুধু demo: আসল অ্যাপে এখানে session বা JWT পড়া হয়
const tokens = new Map<string, User>([["example-token", { id: 7, name: "Rima" }]]);
export function createContext({ req }: CreateExpressContextOptions) {
const token = req.headers.authorization?.replace("Bearer ", "");
return { user: token ? (tokens.get(token) ?? null) : null };
}
type Context = Awaited<ReturnType<typeof createContext>>;
const t = initTRPC.context<Context>().create({ transformer: superjson });
export const router = t.router;
const timing = t.middleware(async ({ path, type, next }) => {
const start = performance.now();
const result = await next();
const ms = (performance.now() - start).toFixed(1);
console.log(`${type} ${path} ok=${result.ok} ${ms}ms`);
return result;
});
export const publicProcedure = t.procedure.use(timing);
export const protectedProcedure = publicProcedure.use(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED", message: "login required" });
return next({ ctx: { user: ctx.user } }); // এখান থেকে user আর null নয়
});এবার আসল API. লক্ষ করুন costPrice (কেনা দাম)। এটি ব্যবসার গোপন তথ্য, তাই toPublic সেটি বাদ দিয়ে উত্তর সাজায়।
import { TRPCError } from "@trpc/server";
import { createOrderInput, productId } from "@bazarghar/shared";
import { publicProcedure, protectedProcedure, router } from "./trpc.ts";
// server-এর জিনিস: browser bundle-এ কখনো যাওয়া চলবে না
export const DB_URL = "postgres://app:example-secret@127.0.0.1/bazarghar";
const products = [
{ id: 1, name: "মিনিকেট চাল ৫ কেজি", price: 450, costPrice: 400, stock: 12 },
{ id: 2, name: "সয়াবিন তেল ১ লিটার", price: 180, costPrice: 160, stock: 3 },
];
type Row = (typeof products)[number];
const toPublic = ({ id, name, price, stock }: Row) => ({ id, name, price, stock });
export const appRouter = router({
product: router({
list: publicProcedure.query(() => products.map(toPublic)),
byId: publicProcedure.input(productId).query(({ input }) => {
const row = products.find((p) => p.id === input.id);
if (!row) throw new TRPCError({ code: "NOT_FOUND", message: `product ${input.id} not found` });
return toPublic(row);
}),
}),
order: router({
create: protectedProcedure.input(createOrderInput).mutation(({ input, ctx }) => {
const row = products.find((p) => p.id === input.productId);
if (!row) throw new TRPCError({ code: "NOT_FOUND", message: "no such product" });
if (row.stock < input.qty)
throw new TRPCError({ code: "CONFLICT", message: `only ${row.stock} left` });
row.stock -= input.qty;
return { orderId: 501, buyer: ctx.user.name, total: row.price * input.qty, createdAt: new Date() };
}),
}),
});
export type AppRouter = typeof appRouter;| অংশ | কাজ | REST-এ কাছাকাছি কী |
|---|---|---|
router | Procedure-গুলোকে গাছের মতো সাজায় | express.Router() |
| procedure | Client যে function ডাকতে পারে | একটি route handler |
.input(schema) | চলার সময় যাচাই, আর type-ও সেখান থেকে আসে | Zod validation middleware |
.query() | পড়ার কাজ; আমাদের run-এ HTTP GET হয়েছে | GET |
.mutation() | লেখার কাজ; আমাদের run-এ HTTP POST হয়েছে | POST, PATCH, DELETE |
context (ctx) | প্রতি request-এ তৈরি থলি: user, db, request id | req.user, res.locals |
middleware (.use) | Procedure-এর আগে-পরে চলে; ctx-এর type-ও বদলাতে পারে | Express middleware |
শেষ সারিটি খেয়াল করুন। Express-এ req.user আছে কি না, type বলতে পারে না। এখানে protectedProcedure পার হলে ctx.user-এর type User | null থেকে User হয়ে যায়। তাই ctx.user.name লিখতে কোনো ! লাগেনি। এটি পর্ব ২৬-এর পাইপলাইন, শুধু type-সহ। Login-এর আসল ব্যবস্থা আসবে পর্ব ৪৫ আর ৪৬-এ; এখানকার token তালিকা শুধু শেখার জন্য।
Inference ফিল্টার নয়। Procedure যা return করে, তা-ই client-এ যায়। আমরা পরীক্ষায় একটি procedure-এ সরাসরি products ফেরত দিয়েছিলাম, উত্তরে "costPrice":400 চলে এসেছে। toPublic-এর মতো হাতে বাছাই করুন, অথবা .output(schema) ব্যবহার করুন। একই পরীক্ষায় .output()-এর Zod object অজানা field ফেলে দিয়েছে, শুধু id, name আর price গেছে। অনুমতির তত্ত্ব আছে API security-তে।
৫ · Express আর React-এ জোড়া — সত্যিকারের run
Router কোনো নির্দিষ্ট server-এ বাঁধা নয়। Adapter তাকে Express-এর middleware বানিয়ে দেয়। তাই পুরোনো Express অ্যাপের ভেতরেও /trpc পথে tRPC বসানো যায়, পাশের REST route অক্ষত থাকে।
import express from "express";
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "./router.ts";
import { createContext } from "./trpc.ts";
const app = express();
app.use("/trpc", createExpressMiddleware({
router: appRouter,
createContext,
onError({ path, error }) {
if (error.code === "INTERNAL_SERVER_ERROR") console.error(path, error);
},
}));
app.listen(4537, "127.0.0.1", () => console.log("api on http://127.0.0.1:4537/trpc"));এবার client. tRPC-র বর্তমান docs নতুন project-এর জন্য @trpc/tanstack-react-query সুপারিশ করে। এটি আলাদা hook বানায় না, বরং পর্ব ১৯-এর চেনা useQuery আর useMutation-এর জন্য option তৈরি করে।
import { createTRPCContext } from "@trpc/tanstack-react-query";
import type { AppRouter } from "@bazarghar/api/router"; // শুধু type, bundle-এ যাবে না
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();const queryClient = new QueryClient();
function App() {
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [httpBatchLink({
url: "/trpc",
transformer: superjson,
headers: () => ({ authorization: "Bearer example-token" }),
})],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
<Shop />
</TRPCProvider>
</QueryClientProvider>
);
}export function Shop() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const list = useQuery(trpc.product.list.queryOptions());
const one = useQuery(trpc.product.byId.queryOptions({ id: 2 }));
const buy = useMutation(trpc.order.create.mutationOptions({
onSuccess: () => queryClient.invalidateQueries({ queryKey: trpc.product.list.queryKey() }),
}));
// … list.data.map((p) => p.name, p.price) দিয়ে তালিকা,
// আর বোতামে buy.mutate({ productId: p.id, qty: 1 })
}Vite-এর build Edge browser-এ Playwright দিয়ে খুলে একটি “কিনুন” বোতাম চাপা হলো, আর সব network অনুরোধ ধরা হলো। পাশাপাশি একটি ছোট Node client দিয়ে ভুলের পথগুলো পরখ করা হলো।
সত্যিকারের run · tRPC 11.22.1, Express 5.3.0, TanStack Query 5.105.1, React 19.3.1, Vite 8.3.5, Zod 4.6.7 · Edge browser · ~/bazarghar
# browser-এর network (URL-decode করা, পড়ার জন্য ভাঙা)
GET /trpc/product.list,product.byId
?batch=1&input={"0":{"json":null,"meta":{"values":["undefined"],"v":1}},"1":{"json":{"id":2}}}
200 [{"result":{"data":{"json":[{"id":1,"name":"মিনিকেট চাল ৫ কেজি","price":450,"stock":9},…]}}},
{"result":{"data":{"json":{"id":2,"name":"সয়াবিন তেল ১ লিটার","price":180,"stock":3}}}}]
# "কিনুন" চাপার পরে
POST /trpc/order.create?batch=1
GET /trpc/product.list?batch=1&input=…
# পাতায় যা দেখা গেল
মিনিকেট চাল ৫ কেজি: 450 টাকা, স্টকে 8
সয়াবিন তেল ১ লিটার: 180 টাকা, স্টকে 3
অর্ডার 501: মোট 450, বছর 2026
# server-এর log, timing middleware থেকে
query product.list ok=true 0.3ms
query product.byId ok=true 0.4ms
mutation order.create ok=true 0.2ms
query product.list ok=true 0.0ms
তিনটি জিনিস খেয়াল করুন। প্রথমত, দুটি আলাদা useQuery একটিমাত্র HTTP অনুরোধে গেছে। এটি httpBatchLink-এর কাজ: কাছাকাছি সময়ের ডাকগুলো এক অনুরোধে জুড়ে দেওয়া, উত্তর আসে একই ক্রমের array-তে। দ্বিতীয়ত, mutation-এর পরে invalidateQueries তালিকা আবার এনেছে, তাই স্টক কমে পাতায় 8 হয়েছে। তৃতীয়ত, URL-এ input থাকায় অনেক বড় batch URL-এর সীমায় পড়তে পারে; এজন্য httpBatchLink-এ maxURLLength option আছে।
- Component
byId({id:2})tsc যাচাই করে - Proxy
"product.byId"path জমায় - Batch link
GET /trpc/…HTTP অনুরোধ - Adapter
ctx, Zodচলার সময় যাচাই - Resolver
toPublic(row)উত্তর তৈরি - Cache, UI
one.datarender
trpc.product.byId.queryOptions({ id: 2 })
// { id: "2" } লিখলে tsc এখানেই থামাত
// input-এর type এসেছে server-এর Zod schema থেকে
trpc.product.byId → path: ["product", "byId"]
input: { id: 2 }
// browser-এ কোনো procedure তালিকা নেই
GET /trpc/product.list,product.byId
?batch=1
&input={"0":{…},
"1":{"json":{"id":2}}}
authorization: Bearer example-token
createContext → { user: { id: 7, name: "Rima" } }
timing middleware শুরু
productId.parse({ id: 2 }) → ঠিক আছে
// { id: -5 } হলে এখানেই BAD_REQUEST 400
{"result":{"data":{"json":{"id":2,
"name":"সয়াবিন তেল ১ লিটার","price":180,"stock":3}}}}
costPrice নেই, toPublic বাদ দিয়েছে
query product.byId ok=true 0.4ms
one.data?.name → সয়াবিন তেল ১ লিটার one.data-এর type: { id, name, price, stock } | undefined
- Compile-এর সময়। Component
byIdডাকছে{ id: 2 }দিয়ে।tscAppRoutertype পড়ে মিলিয়ে নেয়। ভুল থাকলে কোনো অনুরোধই তৈরি হতো না। এই ধাপে network নেই। - Proxy path জমায়।
trpc.product.byIdকোনো আসল object নয়। Proxy প্রতিটি.ধরে নাম জমায়:product, তারপরbyId. Browser জানেই না server-এ কী কী procedure আছে। - Batch link অনুরোধ পাঠায়। Query বলে GET. একই সময়ের
product.listআরproduct.byIdকমা দিয়ে এক path-এ জুড়ে গেছে, input-এ0আর1হলো তাদের ক্রম। - দরজায় যাচাই। Adapter
createContextচালায়, middleware চলে, তারপর Zod ইনপুট যাচাই করে। Browser-এর পাঠানো জিনিস কখনো বিশ্বাস করা হয় না। Type মুছে গেছে, এই দেয়ালটিই চলার সময়ের সুরক্ষা। - Resolver উত্তর বানায়।
toPublicগোপন field বাদ দেয়। superjson উত্তরকেjsonঘরে রাখে। এখানে Date নেই, তাইmetaঘরটি নেই। Timing middleware log লেখে:ok=true 0.4ms. - Cache আর render. TanStack Query উত্তর cache-এ রাখে, React পাতায় বসায়।
one.data-র type কেউ হাতে লেখেনি: resolver-এর return থেকে এসেছে। ক্রমটি মনে রাখুন: type আগে, network মাঝে, type আবার শেষে।
৬ · Type কীভাবে সীমানা পার হয় — নিজে বানিয়ে দেখুন
আসল tRPC পাতায় চালানো যায় না। কিন্তু তার মূল কৌশল মাত্র কয়েক লাইনের TypeScript. নিচের ব্লকগুলো শেখার জন্য বানানো মডেল, আসল tRPC নয়। প্রথম ব্লকে একটি router-এর মতো object আছে, আর server-এ নাম বদলে গেছে। চালিয়ে দেখুন।
// server-এর দিক: router-এর মতো একটি object (শেখার মডেল)
const router = {
product: {
byId: async (input: { id: number }) => ({
id: input.id,
name: "oil 1L",
priceTaka: 180, // আগে এর নাম ছিল price
}),
},
};
type AppRouter = typeof router; // client পর্যন্ত যায় শুধু type
// client-এর দিক: কোড নয়, শুধু AppRouter জানে
type ById = AppRouter["product"]["byId"];
type Product = Awaited<ReturnType<ById>>;
function label(p: Product): string {
return `${p.name}: ${p.price} taka`;
}
function ask(input: Parameters<ById>[0]) {
return input.id;
}
ask({ id: "2" });
router.product.byId({ id: 2 }).then((p) => console.log(label(p)));Compiler দুটি বার্তা দিয়েছে: Property 'price' does not exist on type '{ id: number; name: string; priceTaka: number; }' আর Type 'string' is not assignable to type 'number'. কেউ Product type হাতে লেখেনি। typeof router কোডের আকৃতিকে type বানিয়েছে। ReturnType function-এর ফেরত type নিয়েছে, যা একটি Promise, আর Awaited তার মোড়ক খুলেছে। Parameters দিয়েছে input-এর type. বাকি কাজ ওই তিনটি ছোট helper-এর। tRPC-র inferRouterOutputs<AppRouter> একই ধারণার পরিণত রূপ।
এবার client বানাই। সমস্যা হলো, type চলার সময় থাকে না। তাহলে api.product.byId(…) চলবে কীভাবে? উত্তর দুই ভাগে। Type-এর ভাগে একটি mapped type router-এর আকৃতি নকল করে। চলার সময়ের ভাগে একটি JavaScript Proxy যেকোনো property পড়া ধরে ফেলে, আর শেষে ডাক পড়লে path-সহ অনুরোধ পাঠায়।
// ছোট typed RPC client: tRPC-র ধারণার শেখার মডেল, আসল tRPC নয়
const router = {
product: { byId: async (input: { id: number }) => ({ id: input.id, name: "oil 1L", price: 180 }) },
order: {
create: async (input: { productId: number; qty: number }) => ({ orderId: 501, total: 180 * input.qty }),
},
};
type AppRouter = typeof router;
// function হলে যেমন আছে তেমনই, object হলে আবার ভেতরে
type Client<R> = { [K in keyof R]: R[K] extends (i: any) => any ? R[K] : Client<R[K]> };
type Send = (path: string, input: unknown) => Promise<unknown>;
function createClient<R>(send: Send, path: string[] = []): Client<R> {
return new Proxy(() => {}, {
get: (_t, key) => createClient(send, [...path, String(key)]),
apply: (_t, _self, args) => send(path.join("."), args[0]),
}) as unknown as Client<R>;
}
// "network": client জানে শুধু path আর JSON
const send: Send = async (path, input) => {
console.log("send", path, JSON.stringify(input));
const [group, name] = path.split(".") as ["order", "create"];
return router[group][name](input as { productId: number; qty: number });
};
const api = createClient<AppRouter>(send);
async function main() {
const p = await api.product.byId({ id: 2 }); // p-র পুরো type জানা
const o = await api.order.create({ productId: p.id, qty: 3 });
console.log(p.name, p.price + 20, o.total.toFixed(2));
}
main();আউটপুট:
send product.byId {"id":2}
send order.create {"productId":2,"qty":3}
oil 1L 200 540.00
p.price + 20 হয়েছে 200, আর o.total.toFixed(2) compile হয়েছে, কারণ compiler জানে দুটোই সংখ্যা। Client<R>-এর extends … ? অংশটি conditional type. as unknown as লাগে, কারণ Proxy-র আসল আকৃতি compiler দেখতে পায় না; এই একটি জায়গায় আমরা নিজ দায়িত্বে প্রতিশ্রুতি দিচ্ছি। আসল tRPC-র ভেতরেও এমন recursive proxy আছে, যা দিয়ে @trpc/client path জমায়।
এবার নকল network বাদ দিই। নিচের ব্লকে Node-এর একটি আসল HTTP server চলে, আর Proxy client fetch দিয়ে তাকে ডাকে। Type নেই বলে ভুল নাম এখানে compile-এর সময় ধরা পড়ে না। তফাতটি দেখুন।
const http = require("node:http");
const procedures = {
"product.byId": ({ id }) => ({ id, name: "oil 1L", price: 180 }),
"product.stock": ({ id }) => ({ id, stock: 3 }),
};
const server = http.createServer((req, res) => {
const url = new URL(req.url, "http://x");
const fn = procedures[url.pathname.slice(1)];
res.setHeader("content-type", "application/json");
if (!fn) { res.statusCode = 404; return res.end('{"error":"NOT_FOUND"}'); }
console.log("server got", req.method, req.url);
res.end(JSON.stringify(fn(JSON.parse(url.searchParams.get("input")))));
});
// client: procedure-এর তালিকা কোথাও নেই, Proxy শুধু path জমায়
function client(base, path = []) {
return new Proxy(() => {}, {
get: (_t, key) => client(base, [...path, key]),
apply: async (_t, _self, [input]) => {
const q = encodeURIComponent(JSON.stringify(input));
const res = await fetch(`${base}/${path.join(".")}?input=${q}`);
if (!res.ok) throw new Error(`${res.status} ${path.join(".")}`);
return res.json();
},
});
}
server.listen(0, "127.0.0.1", async () => {
const api = client(`http://127.0.0.1:${server.address().port}`);
console.log(await api.product.byId({ id: 2 }));
console.log(await api.product.stock({ id: 2 }));
await api.product.price({ id: 2 }).catch((e) => console.log("caught", e.message));
server.close();
});আউটপুট:
server got GET /product.byId?input=%7B%22id%22%3A2%7D
{ id: 2, name: 'oil 1L', price: 180 }
server got GET /product.stock?input=%7B%22id%22%3A2%7D
{ id: 2, stock: 3 }
caught 404 product.price
Client-এ product.stock-এর নাম কোথাও লেখা নেই, তবু ডাক চলেছে। product.price নামে কিছু নেই, সেটি ধরা পড়েছে চলার সময়, 404 হয়ে। এই ব্লকের সঙ্গে আগের ব্লক মেলালেই tRPC-র পুরো ছবি পাবেন: Proxy দেয় চলার ক্ষমতা, আর typeof router দেয় compile-এর সময়ের সুরক্ষা। দ্বিতীয়টি মুছে গেলেও প্রথমটি চলে। তাই bundle-এ server কোড লাগে না।
৭ · নাম বদলালে কী হয় — আর bundle-এ server কোড আছে কি
আসল project-এ ফিরি। ধরুন দাম টাকায় না পয়সায়, এই নিয়ে বিভ্রান্তি কাটাতে price হবে priceTaka. নিচের ধাপগুলো বাজারঘরে সত্যিই চালানো হয়েছে।
const toPublic = ({ id, name, price, stock }: Row) =>
({ id, name, priceTaka: price, stock });
$ npx tsc -p apps/api --noEmit
$ echo $?
0
$ npx tsc -p apps/web --noEmit
apps/web/src/Shop.tsx(21,26): error TS2339:
Property 'price' does not exist on type
'{ id: number; name: string; priceTaka: number; stock: number; }'.
$ echo $?
1
$ npx vite build dist/assets/index-BgFbkS4w.js 293.78 kB │ gzip: 90.77 kB ✓ built in 104ms পাতায়, Edge browser-এ: মিনিকেট চাল ৫ কেজি: টাকা, স্টকে 12 দাম নেই, কোনো error-ও নেই
$ npm run build # typecheck && vite build > tsc -p apps/api --noEmit && tsc -p apps/web --noEmit apps/web/src/Shop.tsx(21,26): error TS2339: … exit 1 # vite build চলেইনি
- বদল।
router.ts-এ শুধু একটি field-এর নাম বদলাল। Client-এর কোডে কেউ হাত দেননি। পুরোনো পথে এখান থেকেই type drift শুরু হতো। - Server সুখী। Server-এর দিক থেকে কোনো ভুল নেই, নতুন নাম বৈধ।
tsc -p apps/api-এর exit code 0. - Client ধরা পড়ল।
Shop.tsx-এর ২১ নম্বর লাইন, ২৬ নম্বর কলাম। Compiler ফাইল, লাইন আর নতুন আকৃতি তিনটিই বলে দিল। কোনো request পাঠাতে হয়নি। এটিই পুরো পর্বের কেন্দ্রের মুহূর্ত। - ফাঁদ: Vite চুপ। Vite-এর build type মুছে কোড বানায়, type মেলায় না। তাই একই ভাঙা কোডে
✓ builtএল। সেই build চালিয়ে দেখা গেল, পাতায় দামের জায়গা ফাঁকা। Reactundefinedকিছুই আঁকে না, তাই কোনো error-ও নেই। ঠিক এই ভুলটিই আমরা ঠেকাতে চেয়েছিলাম। - সমাধান:
tscbuild-এর অংশ। মূলpackage.json-এ"build": "npm run typecheck && npm run build -w @bazarghar/web". এখনtscব্যর্থ হলে&&-এর পরের অংশ চলে না, exit code 1. CI-তেও এই কমান্ডই চলবে (পর্ব ৮১)। Type-safety কাজ করে তখনই, যখন কেউ সত্যিই type মেলায়।
একই সুরক্ষা উল্টো দিকেও আছে। Client-এ buy.mutate({ productId: p.id, quantity: 1 }) লিখলে আসল run-এ এল: error TS2353: Object literal may only specify known properties, and 'quantity' does not exist in type '{ productId: number; qty: number; }'.
Client কি server কোড পেল? Bundle খুলে দেখি
“Client server-কে import করছে” শুনলে ভয় লাগা স্বাভাবিক। router.ts-এ DB_URL আর costPrice আছে। উত্তর আছে import type-এ। এটি TypeScript-কে বলে, এই import শুধু type-এর জন্য, build-এ পুরো লাইনটি মুছে ফেলুন। কথায় বিশ্বাস না করে আমরা তৈরি JavaScript ফাইলে গোপন শব্দগুলো খুঁজলাম, আর ইচ্ছা করে ভুল import-ও করলাম।
সত্যিকারের run · Vite 8.3.5 · আকার একটি স্থানীয় build-এর, আপনার project-এ ভিন্ন হবে
$ cat dist/assets/*.js | grep -c "example-secret"
0
$ cat dist/assets/*.js | grep -c "costPrice"
0
# ভুল import, আর কোডে DB_URL-এর ব্যবহার:
# import { appRouter, DB_URL } from "@bazarghar/api/router";
$ npx tsc -p apps/web --noEmit ; echo $?
0 # tsc-এর কোনো আপত্তি নেই
$ npx vite build
✓ 195 modules transformed. # আগে ছিল 97
dist/assets/index-DWIhlz4B.js 373.30 kB │ gzip: 114.18 kB
$ cat dist/assets/*.js | grep -c "example-secret"
1
$ cat dist/assets/*.js | grep -c "costPrice"
1
apps/web-এর import | আকার | gzip | গোপন শব্দ |
|---|---|---|---|
import type { AppRouter } | 293.78 kB | 90.77 kB | নেই |
| ওপরের সঙ্গে Zod schema ব্যবহার (form-এর জন্য) | 361.08 kB | 110.11 kB | নেই |
import { appRouter, DB_URL } | 373.30 kB | 114.18 kB | আছে |
হিসাবটি করি। Zod নিলে বাড়ে ৩৬১.০৮ − ২৯৩.৭৮ = ৬৭.৩০ kB, আর gzip-এ ১১০.১১ − ৯০.৭৭ = ১৯.৩৪ kB, অর্থাৎ প্রায় ২১.৩%. ধরুন ব্যবহারকারীর গতি ১ Mbps (অনুমান)। তাহলে বাড়তি ১৯.৩৪ × ৮ ÷ ১০০০ ≈ ০.১৫ সেকেন্ড শুধু নামাতে। এটি খারাপ নয়, বিনিময়ে client-এ তাৎক্ষণিক যাচাই পাওয়া যায়। কিন্তু সিদ্ধান্তটি সচেতন হওয়া দরকার। Bundle নিয়ে বিস্তারিত পর্ব ৭৯-এ।
আমাদের tsconfig-এ verbatimModuleSyntax চালু। এটি থাকলে import { AppRouter } (type ছাড়া) লিখলে tsc সরাসরি TS1484 error দেয়, আর Vite-এও build ব্যর্থ হয় MISSING_EXPORT বলে। Option বন্ধ করে দেখা গেল, এই একই লাইন build-এ চুপচাপ মুছে গেছে, আর bundle 293.78 kB-ই রয়েছে। তাহলে option কেন? কারণ এটি ইচ্ছাকে স্পষ্ট করে, কোন import চলার সময়ের আর কোনটি নয়, ভাগ্যের ওপর ছাড়ে না।
দুটি শিক্ষা। tsc সফল মানে কোড নিরাপদ নয়: ভুল import-এ tsc 0 দিয়েছে, অথচ secret bundle-এ চলে গেছে। আর secret কখনো shared কোডের পাশে রাখবেন না; environment variable-এ রাখুন (পর্ব ৩১, পর্ব ৭৮)। CI-তে bundle-এ একটি grep ধাপ রাখাও সস্তা বীমা।
৮ · Date, superjson আর TRPCError
JSON-এ Date নেই। Server new Date() পাঠালে তারে যায় string, আর ওপারে সেটি string-ই থাকে। Map, Set, BigInt-এরও একই দশা। superjson-এর মতো transformer এই সমস্যার সমাধান করে। ধারণাটি সহজ: ডেটার পাশে একটি চিরকুট যায়, “এই ঘরটি আসলে Date.” নিজে বানিয়ে দেখা যাক।
const order = { orderId: 501, createdAt: new Date("2026-11-01T10:30:00Z") };
// ১. সাধারণ JSON: Date পথে string হয়ে যায়
const plain = JSON.parse(JSON.stringify(order));
console.log("plain:", typeof plain.createdAt, plain.createdAt instanceof Date);
try {
console.log(plain.createdAt.getUTCFullYear());
} catch (e) {
console.log("crash:", e.message);
}
// ২. ছোট transformer: ডেটার পাশে চিরকুট (superjson-এর ধারণা)
function serialize(obj) {
const json = {}, meta = {};
for (const [k, v] of Object.entries(obj)) {
if (v instanceof Date) { json[k] = v.toISOString(); meta[k] = "Date"; }
else json[k] = v;
}
return JSON.stringify({ json, meta });
}
function deserialize(text) {
const { json, meta } = JSON.parse(text);
for (const [k, kind] of Object.entries(meta)) {
if (kind === "Date") json[k] = new Date(json[k]);
}
return json;
}
const wire = serialize(order);
console.log("wire:", wire);
const back = deserialize(wire);
console.log("back:", back.createdAt instanceof Date, back.createdAt.getUTCFullYear());
console.log("bytes:", JSON.stringify(order).length, "vs", wire.length);আউটপুট:
plain: string false
crash: plain.createdAt.getUTCFullYear is not a function
wire: {"json":{"orderId":501,"createdAt":"2026-11-01T10:30:00.000Z"},"meta":{"createdAt":"Date"}}
back: true 2026
bytes: 54 vs 91
প্রথম দুই লাইনে Date হারিয়ে গেছে, আর method ডাকতেই crash. Transformer দিয়ে ফিরে এসেছে আসল Date. দাম ৯১ − ৫৪ = ৩৭ byte বাড়তি, এই ছোট object-এ। আসল superjson-এর রূপ প্রায় একই:
সত্যিকারের run · superjson 2.2.6 · order.create-এর উত্তর · curl
{"result":{"data":{"json":{"orderId":501,"buyer":"Rima","total":450,"createdAt":"2026-11-01T17:16:24.704Z"},"meta":{"values":{"createdAt":["Date"]},"v":1}}}}
Transformer দুই প্রান্তে লাগে: server-এ initTRPC…create({ transformer: superjson }), client-এ httpBatchLink({ transformer: superjson }). আর tRPC-র inference এখানে সত্যি কথা বলে। Transformer সরিয়ে দিলে createdAt-এর type আর Date থাকে না, string হয়ে যায়:
সত্যিকারের run · TypeScript 7.0.2 · ~/bazarghar
# শুধু server থেকে superjson সরালে
apps/web/src/Shop.tsx(26,106): error TS2339: Property 'getFullYear' does not exist on type 'string'.
apps/web/src/main.tsx(17,9): error TS2322: Type 'typeof SuperJSON' is not assignable to type 'TypeError<"You must define a transformer on your your `initTRPC`-object first">'.
সাধারণ fetch-এ এই ভুল চলার সময়ের crash. এখানে compile error. আর transformer যদি লাগাতেই না চান, API থেকে ISO string পাঠানো একটি পুরোপুরি বৈধ নকশা।
ভুল জানানো: TRPCError
TRPCError-এর code নিজেই HTTP status-এ মিলে যায়, তাই পর্ব ২৮-এর মতো status হাতে বসাতে হয় না। Node client দিয়ে সব পথ পরখ করা হলো:
সত্যিকারের run · apps/api/src/client-demo.ts, npx tsx দিয়ে · ~/bazarghar
byId 1 -> { id: 1, name: 'মিনিকেট চাল ৫ কেজি', price: 450, stock: 12 }
byId 99 -> ERROR NOT_FOUND 404
guest order -> ERROR UNAUTHORIZED 401
qty 50 -> ERROR BAD_REQUEST 400
qty 5 of oil -> ERROR CONFLICT 409
order {
orderId: 501,
buyer: 'Rima',
total: 900,
createdAt: 2026-11-01T17:16:24.627Z
} | is Date: true
qty 50 Zod-এ আটকেছে (সীমা ১ থেকে ১০)। qty 5 of oil Zod পেরিয়ে স্টকের নিয়মে আটকেছে (স্টকে ৩)। দুটি আলাদা স্তর: আকৃতির যাচাই আর ব্যবসার নিয়ম। আর শেষ লাইনের is Date: true দেখাচ্ছে transformer কাজ করেছে।
Development-এ আমাদের error-এর উত্তরে stack field চলে এসেছিল, ভেতরে পুরো ফাইলের পথসহ। tRPC-র docs অনুযায়ী NODE_ENV production না হলে isDev সত্য থাকে, আর তখনই stack পাঠানো হয়। NODE_ENV=production-এ আবার চালিয়ে দেখা গেল stack নেই। অর্থাৎ production-এ এই variable ঠিক রাখা এখানে নিরাপত্তার বিষয়। চাইলে initTRPC.create({ isDev: false }) স্পষ্ট করে লিখুন। Zod-এর ভুল field ধরে দেখাতে errorFormatter ব্যবহার করা যায়; আমাদের run-এ {"fields":{"id":["Too small: expected number to be >0"]}} এসেছে।
৯ · সীমা আর সিদ্ধান্ত — কখন কোন পথ
tRPC খুব আরামদায়ক, তাই সীমাগুলো সততার সঙ্গে জানা দরকার।
- দুই প্রান্তে TypeScript লাগে। Kotlin, Swift বা Dart client
typeof appRouterপড়তে পারে না। মোবাইল অ্যাপ React Native না হলে সে সাধারণ HTTP দিয়ে tRPC-র নিজস্ব wire format নকল করতে পারে বটে, কিন্তু সব type-সুবিধা হারায়। - Public API-র জন্য নয়। বাইরের partner চান স্থির URL, docs আর চুক্তি। tRPC-র URL আর batch format তার ভেতরের বিষয়। tRPC v10 থেকে v11-এ HTTP-র কিছু আচরণ বদলেছে বলে community-তে জানানো হয়েছে; এমন বদল বাইরের ব্যবহারকারীকে ভাঙবে।
- শক্ত বাঁধন। Coupling এখানে ইচ্ছাকৃত। Backend-এর function-এর নাম বদলালেই frontend ভাঙে। এক দলে এটি উপহার, আলাদা দলে এটি ঝামেলা।
- Versioning নেই। tRPC ধরে নেয় client আর server একসাথে deploy হয়। কিন্তু খোলা browser tab-এ পুরোনো কোড চলতে থাকে। এই version skew compiler দেখে না। আমাদের run-এ পুরোনো build নতুন server-এর সঙ্গে চলেছে, দাম ফাঁকা। নিরাপদ পথ হলো expand-contract: আগে নতুন field যোগ করুন, পুরোনোটি রাখুন, client ছড়িয়ে পড়লে তারপর পুরোনোটি মুছুন। Versioning-এর বিস্তার পর্ব ৩৫-এ।
- বড় router-এ compile সময়। সব কিছু inference হওয়ায় অনেক procedure-এ editor ধীর হতে পারে। আমাদের ছোট project-এ দুটি
tscমিলিয়ে ০.৬ সেকেন্ডের কম লেগেছে (একটি স্থানীয় run), তবে বড় project-এর ইঙ্গিত এটি থেকে পাওয়া যায় না।
ডেলিভারি partner-এর Java সিস্টেম,
মোবাইল অ্যাপ (Kotlin)
→ REST + OpenAPI
ভাষা-নিরপেক্ষ চুক্তি, docs, version
বাজারঘরের admin panel:
React + Express, একই monorepo
→ tRPC
generate ধাপ নেই, নাম বদল সঙ্গে সঙ্গে ধরা
ওয়েব, মোবাইল, TV:
প্রতিটি screen ভিন্ন field চায়
→ GraphQL
client ঠিক করে কী লাগবে; দাম জটিলতা
দুই দল, দুই repo, TypeScript
→ Versioned schema package
অথবা OpenAPI codegen
admin panel ─ tRPC ──┐
├─ service layer
partner API ─ REST ──┘
(দুটিই একই database)
- প্রথমে দেখুন কে ডাকবে। এই প্রশ্নটিই সবচেয়ে কঠিন বাধা। বাইরের ব্যবহারকারী বা অন্য ভাষার client থাকলে চুক্তি লাগবে। তখন REST ও OpenAPI সাধারণত নিরাপদ পছন্দ।
- সব আপনার হাতে? দুই প্রান্তে TypeScript, এক repo, একসাথে deploy হলে tRPC সবচেয়ে কম ঝামেলার। বাজারঘরের admin panel ঠিক এমন।
- Client অনেক, চাহিদা ভিন্ন? প্রতিটি screen আলাদা আকৃতির ডেটা চাইলে GraphQL-এর সুবিধা বাড়ে। দাম হলো server-এ N+1 আর cache-এর জটিলতা (পর্ব ৩৬)।
- TypeScript আছে, কিন্তু দল আলাদা? তখন inference-এর বদলে একটি চুক্তি ভালো: version-সহ schema package অথবা OpenAPI থেকে generate. বদল সচেতনভাবে আসে।
- এটি এক-বা-অন্য নয়। দুটি পথ একই service layer (পর্ব ২৯) ব্যবহার করতে পারে। Procedure আর REST controller দুটোই পাতলা থাকলে পরে পথ বদলানোও সস্তা।
| বিষয় | REST + OpenAPI | GraphQL | tRPC |
|---|---|---|---|
| চুক্তি থাকে | আলাদা spec ফাইল | SDL schema | TypeScript কোডেই |
| Client-এর ভাষা | যেকোনো | যেকোনো | কার্যত TypeScript |
| Generate ধাপ | সাধারণত লাগে | সাধারণত লাগে | লাগে না |
| HTTP cache, CDN | সহজ | বাড়তি কাজ | Query GET হলেও URL ভিন্নধর্মী, batch বাড়তি বিবেচনা |
| Public API | খুব মানানসই | মানানসই | সাধারণত নয় |
এই বিষয়ে community-তে মতভেদ আছে। অনেকে বলেন শুরুতে tRPC, দরকার হলে পরে REST. অনেকে বলেন প্রথম দিন থেকে চুক্তি। দুই পক্ষের যুক্তিই ঠিক; পার্থক্য আপনার দলের আকার আর ক্রেতার ধরনে।
পাশের দুই-তিনটি পথ
- Next.js Server Actions: Next.js-এ tRPC-র কিছু কাজ এখন framework নিজেই করে। Server-এর একটি function client থেকে ডাকা যায়, type-সহ। পর্ব ৫৪-এ পুরোটা আসবে।
- Hono RPC: Hono framework-এ route-এর type export করে
hc<AppType>()client পাওয়া যায়, URL থাকে REST-এর মতোই। Docs অনুযায়ী দুই প্রান্তেstrictলাগে। এখানে চালানো হয়নি। - ts-rest: REST route-এর জন্য একটি ভাগ করা contract, যার থেকে OpenAPI-ও তৈরি করা যেত। সতর্কতা: লেখার সময়ে ts-rest.com জানায় প্রকল্পটি আর রক্ষণাবেক্ষণ হচ্ছে না। নতুন project-এ নেওয়ার আগে বর্তমান অবস্থা দেখে নিন। এখানে চালানো হয়নি।
- tRPC-তে ফাইল: v11 থেকে FormData আর বাইনারি পাঠানো যায়, তবে docs অনুযায়ী
httpBatchLink-এর সঙ্গেsplitLinkলাগে। বড় upload-এর জন্য পর্ব ৩০-এর পথ এখনো সরল।
আরও গভীরে: আমাদের run-এ BAD_REQUEST-এর JSON উত্তরে "code":-32600 এসেছিল। সংখ্যাটি JSON-RPC 2.0 spec-এর “Invalid Request” কোড, tRPC সেখান থেকে error-এর ধারণা নিয়েছে। inference-এর খরচ আসে TypeScript-এর type instantiation থেকে: প্রতিটি generic-এর প্রতিটি ব্যবহার compiler-কে আবার হিসাব করতে হয়। বড় monorepo-তে দলগুলো TypeScript project references বা .d.ts তৈরি করে server-এর type আগে থেকে “জমিয়ে” রাখে, যাতে client-কে পুরো server কোড পড়তে না হয়। tsc --extendedDiagnostics এই খরচ মাপার সূচনা।
পরীক্ষা ও ইন্টারভিউতে:
- tRPC কি API schema তৈরি করে? না। Client server-এর TypeScript type সরাসরি পড়ে, কোনো generate ধাপ নেই।
- Type থাকলে Zod কেন? Type চলার সময় মুছে যায়, আর network-এর ইনপুট বিশ্বাসযোগ্য নয়।
- Server কোড bundle-এ যায় না কেন?
import typebuild-এ মুছে যায়, client চলে Proxy আর HTTP-তে। httpBatchLinkকী করে? কাছাকাছি সময়ের একাধিক ডাক এক HTTP অনুরোধে পাঠায়।- tRPC কখন নয়? Public API, অন্য ভাষার client, স্বাধীনভাবে deploy হওয়া দল।
১০ · AI-এর যুগে type-safe API
tRPC-র major version কয়েকবার বদলেছে, তাই AI প্রায়ই পুরোনো কোড দেয়। সুখবর হলো, এখানে compiler নিজেই AI-এর বড় পরীক্ষক। কিন্তু compiler যা দেখে না, সেটি আপনাকেই দেখতে হবে।
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
createTRPCClient({ transformer: superjson }), পুরোনো জায়গায় | আসল run-এ tsc বলল: The transformer property has moved to httpLink/httpBatchLink/wsLink |
@trpc/react-query-র trpc.x.useQuery() নতুন project-এ | Docs এটিকে “Classic” বলে: সমর্থিত, তবে নতুন project-এ সুপারিশ করে না |
import { AppRouter }, type ছাড়া, বা পুরো router import | verbatimModuleSyntax চালু রাখা, bundle-এ grep |
| Database-এর পুরো row ফেরত দেওয়া | Network tab বা curl দিয়ে উত্তরের সব field পড়া; .output() বসানো |
publicProcedure-এ লেখার কাজ, অনুমতি যাচাই নেই | Login ছাড়া ডেকে দেখা; সঠিক ফল UNAUTHORIZED |
as any দিয়ে type error মুছে ফেলা | Diff-এ as any আর @ts-ignore খোঁজা; inference সেখানেই কেটে যায় |
নিচের prompt-এ version আর সীমা স্পষ্ট বলা আছে। শেষ দুটি লাইন খেয়াল করুন: AI-কে কোড লিখতে নয়, নিজের ঝুঁকি দেখাতে বলা হচ্ছে।
AI-এর tRPC কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১RPC দূরের function-কে নিজের function-এর মতো দেখায়। এই সুবিধার ভেতরেই কি একটি বিপদ লুকিয়ে আছে?
RPC-র ধারণা পুরোনো। ১৯৮৪ সালে Andrew Birrell আর Bruce Nelson-এর “Implementing Remote Procedure Calls” গবেষণাপত্র এটিকে বাস্তব রূপ দেয়। লক্ষ্য ছিল, দূরের ডাক যেন সাধারণ function ডাকের মতো সহজ হয়। এরপর এসেছে Sun RPC, CORBA, SOAP, আর ২০১৫ সালে Google-এর gRPC. প্রায় প্রতিটি প্রজন্ম একই স্বপ্ন দেখেছে।
১৯৯৪ সালে Sun Microsystems-এর Jim Waldo ও সহকর্মীরা “A Note on Distributed Computing”-এ সতর্ক করেন: স্থানীয় ডাক আর দূরের ডাককে এক করে দেখানো একটি ভ্রান্তি। পার্থক্যগুলো মৌলিক। দূরের ডাকে সময় অনেক বেশি লাগে। Memory ভাগ করা যায় না। আর সবচেয়ে বড় কথা, আংশিক ব্যর্থতা ঘটে: ডাকটি পৌঁছেছে, কাজ হয়েছে, কিন্তু উত্তর হারিয়ে গেছে। স্থানীয় function-এ এমন হয় না।
api.order.create(…) দেখতে একটি সাধারণ function. কিন্তু উত্তর না এলে অর্ডার হয়েছে কি না, client জানে না। আবার চেষ্টা করলে দুটি অর্ডার হতে পারে। এজন্য idempotency key লাগে (পর্ব ৩৩)। লুপের ভেতরে byId ডাকলে কোড সুন্দর দেখায়, অথচ প্রতিটি ডাক network-এ যায়। tRPC-র batch এটিকে কিছুটা ঢাকে, দূর করে না।
tRPC অবশ্য একটি ক্ষেত্রে আগের প্রজন্মের চেয়ে সৎ। ডাকটি await করতে হয়, আর useQuery-তে loading ও error অবস্থা সামনেই থাকে। Network লুকানো আছে কোডের গঠনে, UI-এর আচরণে নয়।
মূল উপলব্ধি: RPC কোড লেখা সহজ করে, কিন্তু network-এর পদার্থবিদ্যা বদলায় না। ডাকটি দেখতে function হলেও ভাবতে হবে একটি চিঠির মতো: দেরিতে আসতে পারে, হারাতে পারে, দুবার পৌঁছাতে পারে।
প্র ০২চুক্তি আগে লিখে তা থেকে কোড, নাকি কোড লিখে তা থেকে চুক্তি? দুই দর্শনের লড়াইটা কোথায়?
Schema-first পথের ইতিহাস লম্বা। CORBA-র IDL, SOAP-এর WSDL, Google-এর Protocol Buffers, GraphQL-এর SDL: সবখানে একটি নিরপেক্ষ ভাষায় চুক্তি লেখা হয়, তারপর প্রতিটি ভাষার জন্য কোড তৈরি হয়। সুবিধা হলো, চুক্তিটি কোনো একটি ভাষার সম্পত্তি নয়। ইচ্ছা করেই নকশা নিয়ে আগে ভাবতে হয়, আর frontend দল backend তৈরির আগেই কাজ শুরু করতে পারে। অসুবিধা হলো, একই জিনিস দুবার লেখার মতো অনুভূতি আর একটি বাড়তি ধাপ।
Code-first পথে চুক্তি কোড থেকে বের হয়। FastAPI-তে Python type থেকে OpenAPI আসে (পর্ব ৬৯)। tRPC এর চরম রূপ: চুক্তি ফাইলই নেই, TypeScript-এর type system নিজেই চুক্তি। এটি সম্ভব হয়েছে ২০১৮ সালে TypeScript 2.8-এ আসা conditional type আর infer-এর কারণে। এর আগে type-কে প্রোগ্রামের মতো হিসাব করানো এত সহজ ছিল না।
লড়াইটি আসলে গতি বনাম ইচ্ছাকৃত নকশা-র। কোড থেকে আসা চুক্তিতে একটি ভেতরের নাম বদলালে বাইরের চুক্তিও বদলে যায়, হয়তো কেউ না চাইতেই। Hyrum Wright-এর নামে পরিচিত একটি পর্যবেক্ষণ আছে: API-র ব্যবহারকারী যথেষ্ট হলে আপনার সিস্টেমের প্রতিটি দেখা যায় এমন আচরণের ওপর কেউ না কেউ নির্ভর করবে। বাইরের জন্য তাই চুক্তিকে আলাদা, সচেতন বস্তু রাখা নিরাপদ।
আবার ভেতরের admin panel-এ, যেখানে একই মানুষ দুই পাশে কাজ করেন, চুক্তি ফাইলের ওপর আলোচনা সময়ের অপচয় হতে পারে। সেখানে নাম বদলে compiler-এর লাল দাগ ধরে ধরে সব ঠিক করা অনেক দ্রুত।
মূল উপলব্ধি: প্রশ্নটি “কোনটি সেরা” নয়, প্রশ্ন হলো সীমানাটি কত দামি। সীমানা পার হওয়া যত কঠিন, চুক্তি তত স্পষ্ট হওয়া দরকার।
প্র ০৩Compiler সবুজ, deploy-ও ঠিকঠাক। তবু ব্যবহারকারীর পাতা ভাঙে কীভাবে? Type-safety-র সীমা কোথায়?
Compiler একটি নির্দিষ্ট মুহূর্তের কোড দেখে: আপনার repository-র এখনকার অবস্থা। চলমান সিস্টেম কিন্তু একাধিক version-এর মিশ্রণ। ধরুন সকাল ১০টায় নতুন server গেল। রিমা ৯টা থেকে একটি tab খুলে রেখেছেন। তাঁর browser-এ পুরোনো JavaScript চলছে, যা price খোঁজে। Server পাঠাচ্ছে priceTaka. আমাদের run-এ ঠিক এই ঘটনাই দেখা গেছে: পাতায় দাম ফাঁকা, কোনো error নেই।
এর নাম version skew. ধাপে ধাপে deploy হলে কিছুক্ষণ পুরোনো আর নতুন server একসাথে চলে, তাই উল্টো সমস্যাও হয়: নতুন client পুরোনো server-কে ডাকে। মোবাইল অ্যাপে এই জানালা সপ্তাহ বা মাসজুড়ে খোলা থাকতে পারে, কারণ ব্যবহারকারী কবে update করবেন তা আপনার হাতে নেই। tRPC-র জন্য মোবাইল কঠিন হওয়ার এটিও একটি কারণ।
সমাধান হলো সামঞ্জস্য রেখে বদলানো, যাকে expand-contract বলে। প্রথম ধাপে server দুটি field-ই পাঠায়। দ্বিতীয় ধাপে client নতুন field পড়ে, আর সেটি সবার কাছে পৌঁছায়। তৃতীয় ধাপে log বা metrics দেখে নিশ্চিত হয়ে পুরোনো field মোছা হয়। Database-এর migration-এও একই নকশা ব্যবহার হয় (পর্ব ৪০)। tRPC-তে এই ধাপগুলোতে compiler একটু কম সাহায্য করে, কারণ সে ধরে নেয় সবাই সবসময় সর্বশেষ কোডে আছে।
আরও একটি উপায় হলো পুরোনো client-কে পাতা reload করতে বলা। অনেক অ্যাপ server থেকে build-এর পরিচয় পাঠিয়ে মেলায়, না মিললে “নতুন version এসেছে” বার্তা দেখায়।
মূল উপলব্ধি: Type-safety প্রমাণ করে একই commit-এর দুই প্রান্ত মেলে। চলমান সিস্টেমে দুটি ভিন্ন commit একসঙ্গে কথা বলে। সেই ফাঁক compiler নয়, নকশা দিয়ে বন্ধ করতে হয়।
প্র ০৪Server-এর ইনপুট Zod দিয়ে যাচাই হয়। তাহলে উত্তরও প্রতিবার যাচাই করা উচিত নয় কি?
TypeScript-এর নিজের নকশা-লক্ষ্যে স্পষ্ট বলা আছে, চলার সময়ে সে কোনো বাড়তি বোঝা যোগ করবে না। Type মুছে যায় (পর্ব ০৬)। তাই সীমানায় আলাদা যাচাই লাগে। Colin McDonnell ২০২০ সালের দিকে Zod তৈরি করেন ঠিক এই ফাঁকের জন্য: একবার schema লিখে যাচাই আর type দুটোই পাওয়া। ইনপুটে যাচাই নিয়ে বিতর্ক নেই, কারণ ইনপুট আসে অবিশ্বস্ত জগৎ থেকে।
উত্তর ভিন্ন। আপনার resolver-এর return type compiler আগেই জানে, তাই আকৃতির ভুল প্রায় সবসময় compile-এ ধরা পড়ে। তাহলে .output() কেন? তিনটি কারণে। প্রথমত, ফাঁস ঠেকানো: আমাদের পরীক্ষায় Zod object costPrice ফেলে দিয়েছে। এটি শেষ দেয়াল। দ্বিতীয়ত, type ফাঁকি: কোথাও as any বা JSON.parse থাকলে compiler অন্ধ, চলার সময়ের যাচাই ধরে। তৃতীয়ত, বাইরের ডেটা: অন্য service-এর উত্তর পাঠালে তার আকৃতি নিশ্চিত নয়।
দাম হলো CPU. প্রতিটি উত্তরে একবার করে পুরো object হাঁটতে হয়। বড় তালিকায় এটি মাপার মতো হতে পারে, যদিও সাধারণ API-তে database-এর সময়ের তুলনায় তা ছোট বলেই ধরা হয়; নিজের project-এ মেপে দেখাই ভালো। ব্যর্থ হলে client সাধারণত INTERNAL_SERVER_ERROR পায়, কারণ দোষটি server-এর।
একটি মাঝামাঝি নিয়ম কাজে আসে। গোপন field-ওয়ালা জিনিস, অর্থাৎ user, product-এর কেনা দাম, payment, এসবের উত্তরে .output() রাখুন, অথবা database query-তেই শুধু দরকারি column নিন (পর্ব ৪২)। শুধু খোলা ডেটার ক্ষেত্রে inference-ই যথেষ্ট হতে পারে।
মূল উপলব্ধি: ইনপুট যাচাই করা হয় অবিশ্বাস থেকে, উত্তর যাচাই করা হয় নিজের ভুলের ভয় থেকে। দুটির কারণ আলাদা, তাই সিদ্ধান্তও আলাদা।
প্র ০৫Monorepo আর tRPC কি দলের গঠনের ওপরও প্রভাব ফেলে? “শক্ত বাঁধন” কখন উপহার, কখন বোঝা?
১৯৬৮ সালে Melvin Conway “How Do Committees Invent?” প্রবন্ধে লেখেন, সংস্থা যে সিস্টেম বানায়, তার গঠন সংস্থার যোগাযোগের গঠনের নকল হয়। একে এখন Conway's law বলা হয়। API-র সীমানা তাই শুধু প্রযুক্তিগত নয়, এটি দুই দলের মধ্যে একটি সামাজিক চুক্তি।
এক বা দুজনের দলে frontend আর backend আসলে একই মাথায়। সেখানে আলাদা চুক্তি মানে নিজের সঙ্গে নিজে চিঠি লেখা। tRPC-র শক্ত বাঁধন এখানে উপহার: backend-এর নাম বদলের সঙ্গে একই pull request-এ frontend ঠিক হয়, আর compiler প্রমাণ করে কিছু বাদ পড়েনি। বিপরীতে পর্ব ০৪-এর ধারণা মনে করুন: যা একসাথে বদলায়, তা একসাথে রাখা ভালো।
দল বড় হলে ছবি বদলায়। Payment দলের একটি নাম বদল যদি অন্য দশটি দলের build ভাঙে, তখন সেই বাঁধন বোঝা। স্বাধীনভাবে deploy করার ক্ষমতা হারিয়ে যায়, যা অনেক সংস্থার কাছে গতির মূল উৎস। তখন কেউ কেউ tRPC-কে শুধু একটি BFF হিসেবে রাখেন: নির্দিষ্ট ওয়েব অ্যাপের ঠিক সামনে একটি পাতলা tRPC স্তর, যার পেছনে অন্য service-গুলো স্থির চুক্তিতে কথা বলে। এতে ওয়েব দল গতি পায়, আর ভেতরের সীমানা শক্ত থাকে।
Monorepo নিজেও দাম চায়। Build-এর সময় বাড়ে, access control জটিল হয়, CI-তে কেবল বদলে যাওয়া package চালানোর ব্যবস্থা লাগে। কিছু বড় প্রতিষ্ঠান monorepo-তে বিপুল বিনিয়োগ করে সুবিধা পায়, ছোট দলের জন্য npm workspaces সাধারণত যথেষ্ট।
মূল উপলব্ধি: বাঁধন ভালো বা খারাপ নয়, প্রশ্ন হলো সেটি কার সঙ্গে। একই দলের ভেতরে শক্ত বাঁধন গতি দেয়, দলের সীমানা পেরিয়ে সেটি গতি কেড়ে নেয়। API-র নকশা তাই দলের মানচিত্র দেখে করতে হয়।
অনুশীলন
হিসাব, পর্যালোচনা, ফলাফল অনুমান আর একটি কমান্ডের কাজ। আগে নিজে চেষ্টা করুন।
-
Bundle-এর হিসাব: একটি client-এর bundle gzip-এ ৯০.৭৭ kB. পুরো router ভুল করে import করায় তা হলো ১১৪.১৮ kB. (ক) বৃদ্ধি কত kB আর কত শতাংশ? (খ) ব্যবহারকারীর গতি ২ Mbps ধরে শুধু নামাতে বাড়তি কত সময় লাগে? (গ) দিনে ৫০,০০০ বার নতুন করে bundle নামানো হলে (অনুমান) দিনে বাড়তি কত MB যায়?
(ক) ১১৪.১৮ − ৯০.৭৭ = ২৩.৪১ kB. শতাংশ: ২৩.৪১ ÷ ৯০.৭৭ × ১০০ ≈ ২৫.৮%.
(খ) ২৩.৪১ kB = ২৩.৪১ × ৮ = ১৮৭.২৮ kbit. ২ Mbps = ২০০০ kbit/s. সময় = ১৮৭.২৮ ÷ ২০০০ ≈ ০.০৯৪ সেকেন্ড, প্রায় ৯৪ ms.
(গ) ২৩.৪১ × ৫০,০০০ = ১১,৭০,৫০০ kB ≈ ১১৭০.৫ MB (১ MB = ১০০০ kB ধরে)।
তবে এই ক্ষেত্রে আকার আসল সমস্যা নয়। আসল সমস্যা হলো server-এর secret আর কেনা দাম সবার হাতে চলে যাওয়া।
-
কোড পর্যালোচনা: নিচের procedure-এ অন্তত চারটি সমস্যা খুঁজুন:
getOrders: publicProcedure.query(({ input }: any) => db.order.findMany({ where: { userId: input.userId } }))- অনুমতি নেই:
publicProcedure, তাই login ছাড়াই ডাকা যায়। হওয়া উচিতprotectedProcedure. - অন্যের ডেটা:
userIdআসছে client থেকে। যে কেউ অন্যের নম্বর পাঠিয়ে অর্ডার দেখতে পারে।ctx.user.idব্যবহার করতে হবে। একে বলে broken access control, দেখুন পর্ব ৪৮। - যাচাই নেই:
.input()নেই, আরanyসব type-সুবিধা মুছে দিয়েছে। - সীমা নেই:
findMany-এ কোনোtakeনেই, তাই বিশাল তালিকা আসতে পারে (পর্ব ৩৪)। - পুরো row: সব column ফেরত যাচ্ছে, হয়তো এমন কিছুও যা দেখানোর কথা নয়।
selectবা.output()লাগবে।
- অনুমতি নেই:
-
আউটপুট অনুমান করুন: Node.js-এ
console.log(JSON.parse(JSON.stringify({ at: new Date(0), note: undefined, tags: new Set([1, 2]) })))কী ছাপবে? কোন কোন তথ্য হারাল?সত্যিকারের run-এর ফল:
{ at: '1970-01-01T00:00:00.000Z', tags: {} }তিনটিই বদলেছে।
atএখন string.noteপুরোপুরি গায়েব, কারণJSON.stringifyundefined-এর field বাদ দেয়।tagsহয়েছে ফাঁকা object, Set-এর সব মান হারিয়ে গেছে। superjson এই তিনটিই ফিরিয়ে আনে, কারণ সেmetaঘরে আসল ধরন লিখে রাখে। -
কমান্ডের কাজ: বাজারঘরের মতো monorepo-তে
order.create-এর উত্তরেরtotalবদলেtotalTakaকরবেন, আর production-এ browser tab খোলা থাকতে পারে। কোন ক্রমে কী করবেন, প্রতি ধাপে কোন কমান্ড চালাবেন?- Expand: server-এ দুটি field-ই ফেরত দিন:
{ total, totalTaka: total }.npm run typecheckসবুজ থাকবে। Deploy করুন। - Client:
apps/web-এbuy.data.totalবদলেtotalTakaকরুন।npm run buildচালিয়ে deploy করুন। - অপেক্ষা: পুরোনো build-এর ব্যবহার কমেছে কি না log-এ দেখুন। Tab-এর আয়ু আপনার অ্যাপের ওপর নির্ভর করে।
- Contract: server থেকে
totalমুছুন। আবারnpm run typecheck. কোথাও পুরোনো নাম থাকলে এখনই TS2339 আসবে। Deploy করুন।
এক ধাপে নাম বদলালে compiler খুশি থাকত, কিন্তু খোলা tab-গুলোতে মোট ফাঁকা দেখাত।
- Expand: server-এ দুটি field-ই ফেরত দিন:
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি tRPC query-র ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
tscserver-এর type দেখে input মিলিয়ে নেয়- Browser-এ Proxy procedure-এর path জমায়
httpBatchLinkHTTP অনুরোধ পাঠায়- Adapter context তৈরি করে, middleware চলে
- Zod চলার সময় ইনপুট যাচাই করে
- Resolver উত্তর বানায়, client cache-এ রেখে render করে
TypeScript client-এ লেখা হলো const p = (await res.json()) as Product;. Server price-এর নাম বদলে priceTaka করল। কী ঘটবে?
- চলার সময়
aserror দেবে tscচুপ থাকবে, চলার সময়p.priceহবেundefinedtscসঙ্গে সঙ্গে error দেবেres.json()নাম মিলিয়ে নেবে
tRPC-তে .input(z.object({ id: z.number() })) ব্যবহার করা হলো। চলার সময় browser থেকে { id: "abc" } এলে কী হবে?
- TypeScript compiler অনুরোধটি থামাবে
- Zod string-কে সংখ্যায় বদলে resolver চালাবে
- Zod ব্যর্থ হবে, client পাবে
BAD_REQUEST(400) - Resolver চলবে, পরে 500 আসবে
React client-এ শুধু import type { AppRouter } from "@bazarghar/api/router"; আছে। Router ফাইলে একটি secret string আছে। Build-এর পরে bundle-এ কী পাওয়া যাবে?
- Router-এর কোনো কোড বা secret পাওয়া যাবে না
- পুরো router আর secret পাওয়া যাবে
- Secret বাদে router-এর type পাওয়া যাবে
- Bundler secret চিনে মুছে দেবে, বাকি কোড থাকবে
Server-এ একটি field-এর নাম বদলানো হলো। tsc -p apps/web error দিচ্ছে, অথচ vite build সফল। কেন?
- TypeScript-এ বাগ আছে
- Vite পুরোনো cache থেকে build করেছে
- tRPC চলার সময় নাম ঠিক করে নেবে
- Vite type যাচাই করে না, শুধু type মুছে bundle বানায়
tRPC-র একটি procedure { createdAt: new Date() } ফেরত দেয়। কোনো transformer নেই। Client-এ createdAt-এর inferred type কী, আর কেন?
Date, কারণ server-এ তা-ই আছেany, কারণ JSON-এর type নেইstring, কারণ JSON-এ Date string হয়ে যায়number, কারণ Date একটি timestamp
একটি অনলাইন শপ ডেলিভারি partner-কে API দেবে, যাদের সিস্টেম Java-তে লেখা। কোন পথ সবচেয়ে মানানসই?
- tRPC, কারণ এতে generate ধাপ লাগে না
- REST আর OpenAPI চুক্তি
- npm workspace-এর ভাগ করা package
- Next.js Server Actions
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
as Productএকটি প্রতিশ্রুতি, যাচাই নয়। ফাঁক বন্ধের তিন পথ: ভাগ করা schema, code generation, inference.- tRPC: client
import typeকরে AppRouter পড়ে, আর চলার সময় চলে Proxy আর HTTP-তে। - Type মুছে যায়, তাই ইনপুটে Zod আর উত্তরে সচেতন বাছাই লাগে। Inference কোনো ফিল্টার নয়।
vite buildtype দেখে না।tsc && vite buildরাখুন, আর bundle-এgrepকরে যাচাই করুন।- tRPC মানায় এক দলের TypeScript অ্যাপে। Public API, মোবাইল আর version skew-এর জন্য চুক্তি ও expand-contract ভাবুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৮ · ORM-এর ধারণা — object আর table, query builder বনাম raw SQLপরবর্তী পর্বডেটাবেস মডিউলের শুরু।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৩৫ · Versioning আর OpenAPIআগের ভিত্তিচুক্তি লেখা, docs আর version.
- System Design · gRPC আর GraphQLগভীরে যানService-এর মধ্যের RPC আর schema-first নকশা।
- Cybersecurity · API securityপাশের ট্র্যাকযেকোনো API-র অনুমতি আর ফাঁসের ঝুঁকি।