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 };