Client (User A) Server Client (User B)
| | |
| User Logs In (Enter Credentials) | |
|------------------------------------------>| |
| | |
| Server Verifies Credentials | |
| and Returns User Data (Token) | |
|<------------------------------------------| |
| | |
| Client Stores Token (User Authenticated) | |
| | |
| Client Makes Request for Blog Posts | |
| (GET /api/blogs) | |
|------------------------------------------>| |
| | |
| Server Fetches Blog Posts from DB | |
| and Returns Data | |
|<------------------------------------------| |
| | |
| Client Displays Blog Posts | |
| | |
| User Creates New Blog Post | |
| (Fills Form and Submits) | |
|------------------------------------------>| |
| | |
| Server Saves New Blog Post to DB | |
| and Returns Success Message | |
|<------------------------------------------| |
| | |
| Client Displays Confirmation | |
| (New Blog Post Created) | |
| | |
| User Makes Request to Edit Blog Post | |
| (GET /api/blogs/:id) | |
|------------------------------------------>| |
| | |
| Server Retrieves Specific Blog Post | |
| from DB | |
|<------------------------------------------| |
| | |
| Client Displays Blog Post for Editing | |
| | |
| User Edits Blog Post and Submits | |
|------------------------------------------>| |
| | |
| Server Updates Blog Post in DB | |
| and Returns Success Message | |
|<------------------------------------------| |
| | |
| Client Displays Confirmation | |
| (Blog Post Updated) | |
| | |
| User Logs Out | |
|------------------------------------------>| |
| | |
| Server Invalidates User Session | |
| and Returns Logout Success | |
|<------------------------------------------| |
| | |
| Client Redirects to Login Page | |
| | |
- User Logs In: Client sends login credentials to the server.
- Server Verifies: The server verifies the credentials and returns a token if successful.
- Fetch Blog Posts: Client sends a request to the server to fetch all blog posts.
- Create New Blog Post: User creates a new blog post which the client sends to the server to store in the database.
- Edit Blog Post: User edits an existing blog post. The client fetches it from the server and submits the changes.
- User Logs Out: Client sends a logout request, and the server invalidates the session.
This flow outlines basic user interactions such as login, viewing blog posts, creating new posts, editing posts, and logging out.
A monorepo (short for monolithic repository) is a single repository that holds multiple modules or projects, often for large applications with several interrelated components, like frontends, backends, and shared libraries.
In the common folder, I used Zod for defining data types in the backend, which are then inferred for use in the frontend. It enhances type safety, ensures consistency, and improves developer experience.
In a monorepo, developers can focus solely on the parts they need without getting bogged down by other sections of the codebase. For example:
- Frontend Developer: They can concentrate on the frontend directory, ignoring backend modules. Most monorepo setups allow fine-grained permissions or configurations so that each team member sees or works only on relevant parts.
- Backend Developer: Similarly, backend developers can focus on backend-specific modules, libraries, or APIs without needing to dive into frontend code.
In short, a monorepo can still be segmented logically to keep developers focused on what’s relevant to their expertise, reducing complexity and improving productivity.
export const x = z.object({
username: z.string(),
password: z.string()
})
export type xParams = z.infer<typeof x>
npm login
npm publish --access=public
npm pack
you can change the package version and publish any update
make declaration=true in tsconfig.json file
- React in the frontend
- Cloudflare workers in the backend
- zod as the validation library, type inference for the frontend types
- Typescript as the language
- Prisma as the ORM, with connection pooling
- Postgres as the database
- jwt for authentication
-
Hono
Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js.
npm create hono@latest
-
Details
Target directory ›
backend
Which template do you want to use? -
cloudflare-workers
Do you want to install project dependencies? … yes Which package manager do you want to use? › npm (or yarn or bun, doesnt matter)
-
Routes
- POST /api/v1/user/signup
- POST /api/v1/user/signin
- POST /api/v1/blog
- PUT /api/v1/blog
- GET /api/v1/blog/:id
- GET /api/v1/blog/bulk
-
Website
-
Code
import { Hono } from 'hono'; // Create the main Hono app const app = new Hono(); app.post('/api/v1/signup', (c) => { return c.text('signup route') }) app.post('/api/v1/signin', (c) => { return c.text('signin route') }) app.get('/api/v1/blog/:id', (c) => { const id = c.req.param('id') console.log(id); return c.text('get blog route') }) app.post('/api/v1/blog', (c) => { return c.text('signin route') }) app.put('/api/v1/blog', (c) => { return c.text('signin route') }) export default app;
-
Procedure
postgresql://connectly_owner:[email protected]/connectly?sslmode=require
-
Details
When there are a number of workers then they start hitting the database simultaneously so we cannot connect to the database directly we need a middle man which is called connection pool.
we cannot have multiple connections to the database but we can have multiple connection with the pool
DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5IjoiNTZjZWU5OTktOTdmYi00MWVkLTlmZWItMGI2MDQ4OGEyNTY4IiwidGVuYW50X2lkIjoiYzkwZWZmOTczNzdjODkxNmRhMGFkYTdlODcxNGZlMTZmMGU2NGIzOWE2NWYwZmYyZDY3MjI2MjlkZjI3MDc4MyIsImludGVybmFsX3NlY3JldCI6IjRjNDBiYzc5LWI2NjctNDVhMi04NjcxLWI2YTM5NzExNmIyZCJ9.j_wVPPugHzXygSPunxnMeWlgl4TFHCr5TeWvu9Ddlgk"
-
Procedure
Make sure you are in the
backend
foldernpm i prisma npx prisma init
Replace
DATABASE_URL
in.env
(Original database url)DATABASE_URL="postgres://avnadmin:password@host/db"
Add
DATABASE_URL
as theconnection pool
url inwrangler.toml
(accelarate one)name = "backend" compatibility_date = "2023-12-01" [vars] DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5IjoiNTZjZWU5OTktOTdmYi00MWVkLTlmZWItMGI2MDQ4OGEyNTY4IiwidGVuYW50X2lkIjoiYzkwZWZmOTczNzdjODkxNmRhMGFkYTdlODcxNGZlMTZmMGU2NGIzOWE2NWYwZmYyZDY3MjI2MjlkZjI3MDc4MyIsImludGVybmFsX3NlY3JldCI6IjRjNDBiYzc5LWI2NjctNDVhMi04NjcxLWI2YTM5NzExNmIyZCJ9.j_wVPPugHzXygSPunxnMeWlgl4TFHCr5TeWvu9Ddlgk"
You should not have your prod URL committed either in .env or in wrangler.toml to github wranger.toml should have a dev/local DB url .env should be in .gitignore
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique name String? password String posts Post[] } model Post { id String @id @default(uuid()) title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String }
npx prisma migrate dev --name init_schema
npx prisma generate --no-enginenpx prisma generate --no-engine
npm install @prisma/extension-accelerate
import { PrismaClient } from '@prisma/client/edge' import { withAccelerate } from '@prisma/extension-accelerate' const prisma = new PrismaClient({ datasourceUrl: env.DATABASE_URL, }).$extends(withAccelerate())
-
-
Procedure
import { PrismaClient } from '@prisma/client/edge' import { withAccelerate } from '@prisma/extension-accelerate' import { Hono } from 'hono'; import { sign } from 'hono/jwt' // Create the main Hono app const app = new Hono<{ Bindings: { DATABASE_URL: string, JWT_SECRET: string, } }>(); app.post('/api/v1/signup', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); try { const user = await prisma.user.create({ data: { email: body.email, password: body.password } }); const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); return c.json({ jwt }); } catch(e) { c.status(403); return c.json({ error: "error while signing up" }); } }) app.post('/api/v1/signin', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); const user = await prisma.user.findUnique({ where: { email: body.email } }); if (!user) { c.status(403); return c.json({ error: "user not found" }); } const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); return c.json({ jwt }); })
-
Procedure
To restrict a middleware to certain routes, you can use the following -
app.use('/message/*', async (c, next) => { await next() })
In our case, the following routes need to be protected -
app.get('/api/v1/blog/:id', (c) => {}) app.post('/api/v1/blog', (c) => {}) app.put('/api/v1/blog', (c) => {})
So we can add a top level middleware
app.use('/api/v1/blog/*', async (c, next) => { await next() })
Write the logic that extracts the user id and passes it over to the main route.
app.use('/api/v1/blog/*', async (c, next) => { const header = c.req.header("authorization") if (header) { const token = header.split(" ")[1] const response = await verify(token, c.env.JWT_SECRET) if (response.id) { next() } else { c.status(403) return c.json({ error: "Unauthorized" }) } } else { console.log("Undefined"); } await next() })
whenever any request pass through /api/v1/blog/(anything) then it has to app through this middle ware first. header takes out the authorization part from the req and splits it takes the token. the token is verified and the id is pulled out from it. if there is an id then next() else return status 403 not found and unauthorized.
whenever we are using cloudflare workers, it creates a bunch of serverless workers/ instances of that application and it is a bad idea to connect all of them to the database. so whenever we have multiple servers/ or multiple minimachine we should not connect them directly to the database. so we rather connect all of them to connection pool which is connected to the database.
.env: this has information which cli will use. i.e when we migrate, generate client, prisma studio.
.wrangler.toml: holds all the neccessary links which our backend application will use.
-
index.js
import { Hono } from 'hono'; import { userRouter } from './routes/user'; import { blogRouter } from './routes/blog'; const app = new Hono(); app.route('/api/v1/user', userRouter) app.route('/api/v1/blog', blogRouter) export default app
-
routes/user.js
import { Hono } from "hono"; import { PrismaClient } from '@prisma/client/edge' import { withAccelerate } from '@prisma/extension-accelerate' import { sign, verify } from 'hono/jwt' export const userRouter = new Hono<{ Bindings: { DATABASE_URL: string, JWT_SECRET: string, } }>(); userRouter.post('/signup', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env.DATABASE_URL, }).$extends(withAccelerate()); const body = await c.req.json(); try { const user = await prisma.user.create({ data: { email: body.email, password: body.password } }); const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); const s = await verify(jwt, c.env.JWT_SECRET) console.log(s); return c.json({ jwt }); } catch (e) { c.status(403); console.log(e); return c.json({ error: "error while signing up", eror: e }); } }) userRouter.post('/signin', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL, }).$extends(withAccelerate()); const body = await c.req.json(); const user = await prisma.user.findUnique({ where: { email: body.email } }); if (!user) { c.status(403); return c.json({ error: "user not found" }); } const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); return c.json({ jwt }); })
-
routes/blog.js
import { Hono } from "hono"; import { sign, verify } from 'hono/jwt' import { PrismaClient } from '@prisma/client/edge' import { withAccelerate } from '@prisma/extension-accelerate' export const blogRouter = new Hono<{ Bindings: { DATABASE_URL: string, JWT_SECRET: string, }, Variables: { userId: string } }>(); blogRouter.use('/*', async (c, next) => { const jwt = await c.req.header('Authorization'); if (!jwt) { c.status(401); return c.json({ error: "unauthorized" }); } const token = jwt.split(' ')[1]; const payload = await verify(token, c.env.JWT_SECRET); if (!payload) { c.status(401); return c.json({ error: "unauthorized" }); } c.set('userId', payload.id as string); await next() }) blogRouter.post('/', async (c) => { const body = await c.req.json() const prisma = new PrismaClient({ datasourceUrl: c.env.DATABASE_URL, }).$extends(withAccelerate()); const post = await prisma.post.create({ data: { title: body.title, content: body.content, published: true, author_id: c.get('userId') } }) return c.json({ id: post.id }) }) blogRouter.put('/', async (c) => { const body = await c.req.json() const prisma = new PrismaClient({ datasourceUrl: c.env.DATABASE_URL, }).$extends(withAccelerate()); const post = await prisma.post.update({ where: { id: body.id }, data: { title: body.title, content: body.content } }) return c.json({ id: post.id, post: post, updated: true }) }) blogRouter.get('/bulk', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env.DATABASE_URL, }).$extends(withAccelerate()); try { const posts = await prisma.post.findMany() return c.json({ posts }) } catch (error) { c.status(403) return c.json({ msg: "Error while fetching posts" }) } }) blogRouter.get('/:id', async(c) => { const id = await c.req.param('id') const prisma = new PrismaClient({ datasourceUrl: c.env.DATABASE_URL, }).$extends(withAccelerate()); try { const post = await prisma.post.findFirst({ where:{ id: parseInt(id) } }) return c.json({ post }) } catch (error) { c.status(403) return c.json({ msg: "Error while fetching post" }) } }) //eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUzMTk3NjRmLWUwOTgtNGJlMS04MjM1LTc0ZTBlYzI4YjA4YSJ9.OsFUAkxOg4uQvGZkd4UZt-FFD_5o17VjFDYOe59OckA //eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI0ZTRmOWQ3LWZlNTctNDM5OS04MTMxLTVlMDlmYmRmMWY0MyJ9.fM2Fb2HIlumIO36nbs-l3WhFOzqcoMaxBq0k-ipPRKI
-
Procedure
npx wrangler login npx wrangler whoami npm run deploy
-
Procedure
mkdir common cd common npm init -y npx tsc --init
"rootDir": "./src", "outDir": "./dist", "declaration": true,
src/index.js
import z from "zod"; export const signupInput = z.object({ email: z.string().email(), password: z.string(), name: z.string().optional(), }); export type SignupType = z.infer<typeof signupInput>; export const signinInput = z.object({ email: z.string().email(), password: z.string(), }); export type SigninType = z.infer<typeof signinInput>; export const createPostInput = z.object({ title: z.string(), content: z.string(), }); export type CreatePostType = z.infer<typeof createPostInput>; export const updatePostInput = z.object({ title: z.string().optional(), content: z.string().optional(), }); export type UpdatePostType = z.infer<typeof updatePostInput>;
tsc -b npm publish --access public
cd backend **npm i your_package_name** import { PrismaClient } from '@prisma/client/edge' import { withAccelerate } from '@prisma/extension-accelerate' import { Hono } from 'hono'; import { sign, verify } from 'hono/jwt' import { signinInput, signupInput, createPostInput, updatePostInput } from "@100xdevs/common-app" // Create the main Hono app const app = new Hono<{ Bindings: { DATABASE_URL: string, JWT_SECRET: string, }, Variables : { userId: string } }>(); app.use('/api/v1/blog/*', async (c, next) => { const jwt = c.req.header('Authorization'); if (!jwt) { c.status(401); return c.json({ error: "unauthorized" }); } const token = jwt.split(' ')[1]; const payload = await verify(token, c.env.JWT_SECRET); if (!payload) { c.status(401); return c.json({ error: "unauthorized" }); } c.set('userId', payload.id); await next() }) app.post('/api/v1/signup', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); const { success } = signupInput.safeParse(body); if (!success) { c.status(400); return c.json({ error: "invalid input" }); } try { const user = await prisma.user.create({ data: { email: body.email, password: body.password } }); const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); return c.json({ jwt }); } catch(e) { c.status(403); return c.json({ error: "error while signing up" }); } }) app.post('/api/v1/signin', async (c) => { const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); const { success } = signinInput.safeParse(body); if (!success) { c.status(400); return c.json({ error: "invalid input" }); } const user = await prisma.user.findUnique({ where: { email: body.email } }); if (!user) { c.status(403); return c.json({ error: "user not found" }); } const jwt = await sign({ id: user.id }, c.env.JWT_SECRET); return c.json({ jwt }); }) app.get('/api/v1/blog/:id', async (c) => { const id = c.req.param('id'); const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const post = await prisma.post.findUnique({ where: { id } }); return c.json(post); }) app.post('/api/v1/blog', async (c) => { const userId = c.get('userId'); const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); const { success } = createPostInput.safeParse(body); if (!success) { c.status(400); return c.json({ error: "invalid input" }); } const post = await prisma.post.create({ data: { title: body.title, content: body.content, authorId: userId } }); return c.json({ id: post.id }); }) app.put('/api/v1/blog', async (c) => { const userId = c.get('userId'); const prisma = new PrismaClient({ datasourceUrl: c.env?.DATABASE_URL , }).$extends(withAccelerate()); const body = await c.req.json(); const { success } = updatePostInput.safeParse(body); if (!success) { c.status(400); return c.json({ error: "invalid input" }); } prisma.post.update({ where: { id: body.id, authorId: userId }, data: { title: body.title, content: body.content } }); return c.text('updated post'); }); export default app;
-
Procedure
npm create vite@latest npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p tailwind.config /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } index.css @tailwind base; @tailwind components; @tailwind utilities; npm i @soumrnjn/connectly-common npm run dev
-
Procedure
npm i react-router-dom
import { BrowserRouter, Route, Routes } from 'react-router-dom' import { Signup } from './pages/Signup' import { Signin } from './pages/Signin' import { Blog } from './pages/Blog' function App() { return ( <> <BrowserRouter> <Routes> <Route path="/signup" element={<Signup />} /> <Route path="/signin" element={<Signin />} /> <Route path="/blog/:id" element={<Blog />} /> </Routes> </BrowserRouter> </> ) } export default App
to reset prisma npx prisma migrate reset