```typescript
import { initTRPC, TRPCError } from "@trpc/server";
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";
// ============================================
// Types
// ============================================
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
interface Post {
id: string;
title: string;
content: string;
authorId: string;
published: boolean;
createdAt: Date;
}
interface Context {
user: User | null;
}
// ============================================
// In-Memory Database
// ============================================
class Database {
users = new Map<string, User & { passwordHash: string }>();
posts = new Map<string, Post>();
private nextUserId = 1;
private nextPostId = 1;
createUser(data: { email: string; name: string; password: string }): User {
const user = {
id: String(this.nextUserId++),
email: data.email,
name: data.name,
passwordHash: `hashed-${data.password}`, // Use bcrypt in production
createdAt: new Date(),
};
this.users.set(user.id, user);
const { passwordHash, ...publicUser } = user;
return publicUser;
}
getUser(id: string): User | undefined {
const user = this.users.get(id);
if (!user) return undefined;
const { passwordHash, ...publicUser } = user;
return publicUser;
}
getUserByEmail(email: string): (User & { passwordHash: string }) | undefined {
return Array.from(this.users.values()).find((u) => u.email === email);
}
getAllUsers(): User[] {
return Array.from(this.users.values()).map(({ passwordHash, ...u }) => u);
}
createPost(data: { title: string; content: string; authorId: string }): Post {
const post: Post = {
id: String(this.nextPostId++),
...data,
published: false,
createdAt: new Date(),
};
this.posts.set(post.id, post);
return post;
}
getPost(id: string): Post | undefined {
return this.posts.get(id);
}
getPostsByAuthor(authorId: string): Post[] {
return Array.from(this.posts.values()).filter(
(p) => p.authorId === authorId,
);
}
getPublishedPosts(): Post[] {
return Array.from(this.posts.values()).filter((p) => p.published);
}
updatePost(id: string, data: Partial<Post>): Post | undefined {
const post = this.posts.get(id);
if (!post) return undefined;
Object.assign(post, data);
return post;
}
deletePost(id: string): boolean {
return this.posts.delete(id);
}
}
const db = new Database();
// ============================================
// tRPC Setup
// ============================================
const t = initTRPC.context<Context>().create();
const publicProcedure = t.procedure;
const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Must be logged in" });
}
return next({ ctx: { ...ctx, user: ctx.user } });
});
// ============================================
// Input Schemas
// ============================================
const registerSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(50),
password: z.string().min(8),
});
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
});
const createPostSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10),
});
const updatePostSchema = z.object({
id: z.string(),
title: z.string().min(3).max(200).optional(),
content: z.string().min(10).optional(),
});
const paginationSchema = z.object({
limit: z.number().min(1).max(100).default(20),
cursor: z.string().optional(),
});
// ============================================
// User Router
// ============================================
const userRouter = t.router({
register: publicProcedure
.input(registerSchema)
.mutation(async ({ input }) => {
if (db.getUserByEmail(input.email)) {
throw new TRPCError({
code: "CONFLICT",
message: "Email already exists",
});
}
const user = db.createUser(input);
return { user, token: `token-${user.id}` };
}),
login: publicProcedure.input(loginSchema).mutation(async ({ input }) => {
const user = db.getUserByEmail(input.email);
if (!user || user.passwordHash !== `hashed-${input.password}`) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid credentials",
});
}
const { passwordHash, ...publicUser } = user;
return { user: publicUser, token: `token-${user.id}` };
}),
me: protectedProcedure.query(({ ctx }) => ctx.user),
list: publicProcedure.query(() => db.getAllUsers()),
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const user = db.getUser(input.id);
if (!user)
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
return user;
}),
});
// ============================================
// Post Router
// ============================================
const postRouter = t.router({
list: publicProcedure.input(paginationSchema).query(({ input }) => {
const posts = db.getPublishedPosts();
return {
items: posts.slice(0, input.limit),
nextCursor:
posts.length > input.limit ? posts[input.limit].id : undefined,
};
}),
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
const post = db.getPost(input.id);
if (!post)
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
return post;
}),
myPosts: protectedProcedure.query(({ ctx }) =>
db.getPostsByAuthor(ctx.user.id),
),
create: protectedProcedure
.input(createPostSchema)
.mutation(({ ctx, input }) => {
return db.createPost({ ...input, authorId: ctx.user.id });
}),
update: protectedProcedure
.input(updatePostSchema)
.mutation(({ ctx, input }) => {
const post = db.getPost(input.id);
if (!post)
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
if (post.authorId !== ctx.user.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "Not your post" });
}
return db.updatePost(input.id, input);
}),
publish: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(({ ctx, input }) => {
const post = db.getPost(input.id);
if (!post)
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
if (post.authorId !== ctx.user.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "Not your post" });
}
return db.updatePost(input.id, { published: true });
}),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(({ ctx, input }) => {
const post = db.getPost(input.id);
if (!post)
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
if (post.authorId !== ctx.user.id) {
throw new TRPCError({ code: "FORBIDDEN", message: "Not your post" });
}
return db.deletePost(input.id);
}),
});
// ============================================
// App Router
// ============================================
const appRouter = t.router({
health: publicProcedure.query(() => ({
status: "healthy",
timestamp: new Date().toISOString(),
})),
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
// ============================================
// Context Creation
// ============================================
function createContext(opts: {
req: { headers: { authorization?: string } };
}): Context {
const token = opts.req.headers.authorization?.replace("Bearer ", "");
if (token?.startsWith("token-")) {
const userId = token.replace("token-", "");
const user = db.getUser(userId);
return { user: user ?? null };
}
return { user: null };
}
// ============================================
// Server
// ============================================
const server = createHTTPServer({
router: appRouter,
createContext,
});
const PORT = process.env.PORT || 3000;
server.listen(PORT);
console.log(`🚀 tRPC Server: http://localhost:${PORT}`);
export { appRouter, createContext, db };
```