A modern social media platform where users can register, log in, post updates, like posts, and follow/unfollow other users. Built with a focus on clean architecture, type safety, and developer experience.
- π React (Vite): For building our dynamic user interfaces
- π TypeScript: Ensuring type safety across the entire application
- π¨ Tailwind CSS: Utility-first styling with DaisyUI extensions
- π― DaisyUI: Provides CSS variables and enhanced utilities like
btn btn-primary
andbg-primary/base/error
. Handles theming through data attributes - π§© Shadcn: Used for complex components due to its modular design and strong accessibility defaults
- View transitions(Chrome and safari only) : That offer nice animations and transitions between different states of the application
- π£οΈ TanStack Router: For strongly typed routes and search params
Why tanstack router over react router
- Type Safety: TanStack Router provides type-safe route params and search params, ensuring that you catch errors at compile time rather than runtime.
- Nested Routes: It supports nested routes, allowing for better organization of your application and more granular control over route matching.
- Data Fetching: It has built-in support for data fetching, making it easier to manage server-side data in your components.
- Most ergonomic File based routing: It allows you to define routes using a file-based structure, which allows for more confiuration that your average file router.
// just by creatinga afile under src/routes/admin , we getsome initila boilerplate code an dit gets auto configerd in our main routes
import { createFileRoute, redirect } from "@tanstack/react-router";
import { ListAllUsers } from "./-components/ListAllUsers";
import { BulkusresCreate } from "./-components/BulkusresCreate";
export const Route = createFileRoute("/admin/")({
component: RouteComponent,
// example of a before loader "middleware" that runs before the route is loaded
beforeLoad(ctx) {
// router context passed i from the top to track global variables
if (ctx.context.viewer?.role !== "admin") {
throw redirect({
to: "/profile",
});
}
},
// it also supposrt loaders and many more options
});
function RouteComponent() {
// Type-safe access to route params
const { username } = Route.useParams()
// or
const { username } = useParams({
from: "/admin",
})
// this would error because the route is not defined
// same applies to search params
const { username } = useParams({
from: "/admi",
})
// Type-safe search params
const { tab } = Route.useSearch()
return (
<div className="min-h-screen flex flex-col items-center gap-3">
<BulkusresCreate/>
<ListAllUsers />
</div>
);
}
-
π TanStack Query: Managing non-GraphQL state with automatic refetching, caching, and invalidation
Why TanStack Query over Redux
- Simplicity: TanStack Query is simpler to set up and use compared to Redux, which requires more boilerplate code and configuration.
- Better devtools: TanStack Query has a powerful embedded devtools for debugging and monitoring API requests.
- Automatic caching and invalidation: TanStack Query automatically caches and invalidates data based on query keys.
- Optimistic updates: Built-in support for optimistic updates, improving user experience during data mutations.
- Smart data refetching: Marks data as stale and triggers refetches on:
- Browser window refocus
- Network reconnection
- Configurable time intervals
- Custom invalidation rules
// filepath: /home/dennis/Desktop/frens/client/src/hooks/use-auth-state.ts
import { useQuery } from '@tanstack/react-query'
export function useAuthState() {
return useQuery({
queryKey: ['authState'],
queryFn: async () => {
const response = await fetch('/api/me')
if (!response.ok) return null
return response.json()
},
staleTime: 1000 * 60 * 60 * 24, // 1 day
})
}
- π Relay GraphQL Client: Type-safe data fetching
// filepath: /home/dennis/Desktop/frens/client/src/components/post-card.tsx
import { useFragment } from 'react-relay'
import { graphql } from 'relay-runtime'
export function PostCard({ post }: { post: PostCard_post$key }) {
const data = useFragment(
graphql`
fragment PostCard_post on Post {
id
content
createdAt
author {
username
avatarUrl
}
likeCount
isLikedByViewer
}
`,
post
)
return (
// Component implementation using data
)
}
Why realy over apollo
- Pagination: Relay's pagination system is more sophisticated than Apollo's:
- Uses cursor-based pagination with Connections and Edges
- Provides built-in hooks for infinite scrolling and load-more patterns
- Handles complex pagination scenarios like:
- Bi-directional pagination (forward/backward)
- Prepending/appending items
- Refetching subsets of data
- Automatic connection management
- Maintains consistency across paginated lists with optimistic updates
- Supports declarative "load more" patterns with useLoadMore hook
- Handles edge cases like:
- Items appearing in multiple connections
- Items being deleted while paginating
- Race conditions in parallel pagination requests
export const MainFeedFragment = graphql`
fragment MainFeed_feedPosts on Query
@argumentDefinitions(first: { type: "Int", defaultValue: 24 }, after: { type: "String" })
@refetchable(queryName: "MainFeedPaginationQuery") {
feedPosts(first: $first, after: $after) @connection(key: "MainFeed_feedPosts", filters: []) {
edges {
cursor
node {
id
...FeedCard_post
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`;
import { MainFeed_feedPosts$key } from './__generated__/MainFeed_feedPosts.graphql';
import { MainFeedPaginationQuery } from "./__generated__/MainFeedPaginationQuery.graphql";
const fragData = usePaginationFragment<MainFeedPaginationQuery, MainFeed_feedPosts$key>(MainFeedFragment, queryRef);
With this simeple code we automatically get pagination and refetching for free
Note
This assumes your apis are relay compliant and has connections on edges of nodes
- π Node.js & Express: Server-side logic and request handling
- π― GraphQL Yoga: Integrated as an Express middleware for GraphQL functionality
- ποΈ Prisma ORM: Database interaction with PostgreSQL
- π PostgreSQL: Main database for storing application data
- π¨ Pothos GraphQL: Code-first GraphQL schema builder with Prisma plugin integration
-
π Authentication (Better Auth):
While implementing authentication within our GraphQL API was considered, i chose Better Auth for several critical reasons:
- Security risks of custom authentication implementations
-
Heavy maintenance overhead for password encryption, token handling, session management
-
Complex flows for password resets and account recovery
-
Integration challenges with multiple OAuth providers
-
Robust API key rotation and management
-
Edge cases in multi-device session handling
-
Advanced security features like MFA and device tracking
-
Real-time session invalidation across devices
-
Rate limiting and brute force protection
- Complexity of maintaining security best practices
Better Auth provides:
- Built-in email/password authentication
- OAuth integration (GitHub enabled)
- Secure API key management
- Production-ready rate limiting
- Session management with cookie caching
- Extensive documentation and active maintenance
- Battle-tested security features
- ποΈ Pothos: Code-first GraphQL schema builder with Prisma plugin integration
- Most of the types oare on the root alongside the
Query
andMutation
types Theres 2 iser variants
Note
Pothos variavts are a way to extend bease types in this case our prisma models and enhance them with more fields and methods , like is followingMe
or am i followingThem
or followersCount
which are nice to ahve ona timeline filed but have to be derived at query time
type Follower implements Node {
amFollowing: Boolean
createdAt: String
email: String!
followerCount: Int
followingCount: Int
frenId: String!
id: ID!
image: String
isFollowingMe: Boolean
isMe: Boolean
name: String!
role: String
}
type Fren implements Node {
amFollowing: Boolean
createdAt: String
email: String!
followerCount: Int
followers(after: String, before: String, first: Int, last: Int, sort: SortInput): FrenFollowersConnection
following(after: String, before: String, first: Int, last: Int, sort: SortInput): FrenFollowingConnection
followingCount: Int
frenId: String!
id: ID!
image: String
isFollowingMe: Boolean
isMe: Boolean
name: String!
posts(after: String, before: String, first: Int, last: Int, sort: SortInput): FrenPostsConnection
postsCount: Int
role: String
}
Why? Because user varaint Follow
will be te return type of the following
and followers
fields on the Fren
type.
Ideally i would love all the User types to be of variant Fren
but self referencing types don' work well with typescript and might lead to infinite depth queries
The prisma pothos
plugin also allows us to create relay compliant APIs with ease
just by defining our model like this
export const Fren = builder.prismaNode("User", {
variant: "Fren",
id: { field: "id" },
fields: (t) => ({
frenId: t.exposeString("id", { nullable: false }),
...
We get
type Fren implements Node {
amFollowing: Boolean
createdAt: String
email: String!
followerCount: Int
....
}
type QueryFeedPostsConnection {
edges: [QueryFeedPostsConnectionEdge]
pageInfo: PageInfo!
}
type QueryFeedPostsConnectionEdge {
cursor: String!
node: FeedPost
}
type QueryFrensConnection {
edges: [QueryFrensConnectionEdge]
pageInfo: PageInfo!
}
type QueryFrensConnectionEdge {
cursor: String!
node: Fren
}
Our pothos instance als inherits context from it's gql server and offers a way to exend it with mutiple plugins like our prisma one
export type PothosBuilderGenericTYpe = {
PrismaTypes: PrismaTypes;
Context: {
currentUser?: Pick<User, "id" | "email" | "name">;
};
};
export const builder = new SchemaBuilder<PothosBuilderGenericTYpe>({
plugins: [PrismaPlugin, RelayPlugin],
relay: {},
prisma: {
client: prisma,
exposeDescriptions: true,
// defaults to false, uses /// comments from prisma schema as descriptions
// for object types, relations and exposed fields.
we then exort this a schema and a string one fed to our graphql yoga server and the oter is eposed as a string to the clent for relay client uses
export type PothosBuilderGenericTYpe = {
PrismaTypes: PrismaTypes;
Context: {
currentUser?: Pick<User, "id" | "email" | "name">;
};
};
export const builder = new SchemaBuilder<PothosBuilderGenericTYpe>({
plugins: [PrismaPlugin, RelayPlugin],
relay: {},
prisma: {
client: prisma,
exposeDescriptions: true,
// defaults to false, uses /// comments from prisma schema as descriptions
// for object types, relations and exposed fields.
// descriptions can be omitted by setting description to false
// exposeDescriptions: boolean | { models: boolean, fields: boolean },
// use where clause from prismaRelatedConnection for totalCount (defaults to true)
filterConnectionTotalCount: true,
// warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === "production" ? null : "warn",
},
});
// .. builder.quetie and builder.mutatios somewhere here
// .. builder.quetie and builder.mutatios somewhere here
export const pothosSchema = builder.toSchema();
// export const schemaAsString = printType(lexicographicSortSchema(pothosSchema))
export const pothosSchemaString = printSchema(
lexicographicSortSchema(pothosSchema),
// .... in index.ts
const yoga = createYoga<{
req: express.Request;
res: express.Response;
}>({
// use the apollo sandbox
renderGraphiQL: () => {
return `
<!DOCTYPE html>
<html lang="en">
<body style="margin: 0; overflow-x: hidden; overflow-y: hidden">
<div id="sandbox" style="height:100vh; width:100vw;"></div>
<script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
<script>
new window.EmbeddedSandbox({
target: "#sandbox",
// Pass through your server href if you are embedding on an endpoint.
// Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
initialEndpoint: "http://localhost:${port}/graphql",
});
// advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
</script>
</body>
</html>`
},
schema: pothosSchema,
context: async (ctx) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(ctx.req.headers),
});
if(!session){
return {
currentUser: null,
};
}
return {
currentUser:{
id: session?.user.id,
email: session?.user.email,
name: session?.user.name,
}
}
},
graphiql: true,
logging: true,
// maskedErrors: false,
cors: true,
});
app.use(yoga.graphqlEndpoint, yoga);
Why graphql yoga over apollo server
- Simplicity: GraphQL Yoga is simpler to set up and use compared to Apollo Server, which requires more configuration and boilerplate code.
-
- cleaner intergration with express middlewware
- While pothos works with apollo server it plays nicer with graphql yoga
Note
Picking an appropriate apprach for graphql api development took me the most time , i ahd to explore and dismisss old assumptions and at a point i even considered just writing my SDLs by hand but i eventually picked this approach and after getting the hang of it i can say it was the best decision i made for this project. I also considered using trpc but i wanted to try something new and i also wanted to have a relay compliant api to make use of the relay client features like optimistic updates and automatic refetching
as forthe rest of the express app we have
app.all("/api/auth/*", toNodeHandler(auth));
//with thtadeclared we can not get the sessionlike so -->
app.get("/api/me", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
res.json(session);
});
// our sessions are cookie based but sould one want to call our grapql api outside the browser (apollo client or postman) they have to provide an api key for the restricted actions
export const auth = betterAuth({
trustedOrigins:allowedOrigins,
database: prismaAdapter(prisma, {
provider: "postgresql", // or "mysql", "postgresql", ...etc
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
},
plugins: [openAPI(), admin(),
apiKey({
// configuring better auth to look for Bearer tokens in the Authorization header
customAPIKeyGetter(ctx) {
const bearer_token = ctx.headers?.get('Authorization')
if(!bearer_token) return null
const token = bearer_token.split(' ')
if(token[0] !== 'Bearer') return null
if(token.length !== 2) return null
return token[1]
},
}) ],
});
// ctx.headers?.get("AUTHORIZATION")
// ctx.headers?.get('Authorization')
// ctx.headers?.get('authorization')
Note
fun fact i learned on this project , these three resolve to the same value
To get your access token on out fronend got to /profile/account and under the settings tab hit create
- No tests
- Some cache invalidation might nor=t work correctly on the frontend
- No replies features
- No email verification
Frontnd on vercel , express server on render Try it out