0% found this document useful (0 votes)
6 views29 pages

Blogging

The document outlines the steps to build a blogging platform using a specific tech stack including React, Cloudflare Workers, and Prisma with PostgreSQL. It details the initialization of the backend, setting up routes for user signup, signin, and blog management, as well as implementing JWT for authentication. Additionally, it covers middleware creation, database schema setup, and organizing routes for better structure.

Uploaded by

loopspell1
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views29 pages

Blogging

The document outlines the steps to build a blogging platform using a specific tech stack including React, Cloudflare Workers, and Prisma with PostgreSQL. It details the initialization of the backend, setting up routes for user signup, signin, and blog management, as well as implementing JWT for authentication. Additionally, it covers middleware creation, database schema setup, and organizing routes for better structure.

Uploaded by

loopspell1
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 29

7/8/24, 11:35 PM DailyCode

Step 1 - The stack


We’ll be building medium in the following stack

1. React in the frontend

2. Cloudflare workers in the backend

3. zod as the validation library, type inference for the frontend types

4. Typescript as the language

5. Prisma as the ORM, with connection pooling

6. Postgres as the database

7. jwt for authentication

https://projects.100xdevs.com/pdf/blog/blog-1 1/29
7/8/24, 11:35 PM DailyCode

Step 2 - Initialize the backend


Whenever you’re building a project, usually the first thing you should do is initialise the project’s
backend.
Create a new folder called medium

mkdir medium Copy


cd medium

Initialize a hono based cloudflare worker app

npm create hono@latest Copy

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)

💡 Reference https://hono.dev/top

https://projects.100xdevs.com/pdf/blog/blog-1 2/29
7/8/24, 11:35 PM DailyCode

Step 3 - Initialize handlers


To begin with, our backend will have 4 routes

1. POST /api/v1/user/signup

2. POST /api/v1/user/signin

3. POST /api/v1/blog

4. PUT /api/v1/blog

5. GET /api/v1/blog/:id

6. GET /api/v1/blog/bulk

💡 https://hono.dev/api/routing

Solution

https://projects.100xdevs.com/pdf/blog/blog-1 3/29
7/8/24, 11:35 PM DailyCode

import { Hono } from 'hono'; Copy

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

Step 4 - Initialize DB (prisma)


1. Get your connection url from neon.db or aieven.tech

postgres://avnadmin:password@host/db Copy

2. Get connection pool URL from Prisma accelerate


https://www.prisma.io/data-platform/accelerate

https://projects.100xdevs.com/pdf/blog/blog-1 4/29
7/8/24, 11:35 PM DailyCode

prisma://accelerate.prisma-data.net/?api_key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ Co

3. Initialize prisma in your project


Make sure you are in the backend folder

npm i prisma Copy


npx prisma init

Replace DATABASE_URL in .env

DATABASE_URL="postgres://avnadmin:password@host/db" Copy

Add DATABASE_URL as the connection pool url in wrangler.toml

name = "backend" Co
compatibility_date = "2023-12-01"

[vars]
DATABASE_URL = "prisma://accelerate.prisma-data.net/?api_key=eyJhbGciOiJIUzI1NiIsInR5

💡 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

4. Initialize the schema

generator client { Copy


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[]
}
https://projects.100xdevs.com/pdf/blog/blog-1 5/29
7/8/24, 11:35 PM DailyCode

model Post {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
}

5. Migrate your database

npx prisma migrate dev --name init_schema Copy

💡 You might face issues here, try changing your wifi if that happens

6. Generate the prisma client

npx prisma generate --no-engine Copy

7. Add the accelerate extension

npm install @prisma/extension-accelerate Copy

8. Initialize the prisma client

import { PrismaClient } from '@prisma/client/edge' Copy


import { withAccelerate } from '@prisma/extension-accelerate'

const prisma = new PrismaClient({


datasourceUrl: env.DATABASE_URL,
}).$extends(withAccelerate())

https://projects.100xdevs.com/pdf/blog/blog-1 6/29
7/8/24, 11:35 PM DailyCode

Step 5 - Create non auth routes


1. Simple Signup route
Add the logic to insert data to the DB, and if an error is thrown, tell the user about it
Solution

app.post('/api/v1/signup', async (c) => { Copy


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

return c.text('jwt here')


} catch(e) {
return c.status(403);
}
})

💡 To get the right types on c.env , when initializing the Hono app, pass the types of env
as a generic

const app = new Hono<{ Copy


Bindings: {
DATABASE_URL: string
}
}>();

💡 Ideally you shouldn’t store passwords in plaintext. You should hash before storing them.
More details on how you can do that -
https://community.cloudflare.com/t/options-for-password-hashing/138077
https://developers.cloudflare.com/workers/runtime-apis/web-crypto/

2. Add JWT to signup route


Also add the logic to return the user a jwt when their user id encoded.
This would also involve adding a new env variable JWT_SECRET to wrangler.toml

https://projects.100xdevs.com/pdf/blog/blog-1 7/29
7/8/24, 11:35 PM DailyCode

💡 Use jwt provided by hono - https://hono.dev/helpers/jwt

Solution

import { PrismaClient } from '@prisma/client/edge' Copy


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

3. Add a signin route


Solution

Copy
app.post('/api/v1/signin', async (c) => {
const prisma = new PrismaClient({
datasourceUrl: c.env?.DATABASE_URL ,
}).$extends(withAccelerate());

const body = await c.req.json();

https://projects.100xdevs.com/pdf/blog/blog-1 8/29
7/8/24, 11:35 PM DailyCode

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

Step 6 - Middlewares
Creating a middleware in hono is well documented - https://hono.dev/guides/middleware

1. Limiting the middleware


To restrict a middleware to certain routes, you can use the following -

app.use('/message/*', async (c, next) => { Copy


await next()
})

In our case, the following routes need to be protected -

Copy
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

https://projects.100xdevs.com/pdf/blog/blog-1 9/29
7/8/24, 11:35 PM DailyCode

app.use('/api/v1/blog/*', async (c, next) => { Copy


await next()
})

2. Writing the middleware


Write the logic that extracts the user id and passes it over to the main route.
How to pass data from middleware to the route handler?
Using the context - https://hono.dev/api/context

How to make sure the types of variables that are being passed is correct?

const app = new Hono<{ Copy


Bindings: {
DATABASE_URL: string,
JWT_SECRET: string,
},
Variables : {
userId: string
}
}>();

Solution

https://projects.100xdevs.com/pdf/blog/blog-1 10/29
7/8/24, 11:35 PM DailyCode

app.use('/api/v1/blog/*', async (c, next) => { Copy


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()
})

3. Confirm that the user is able to access authenticated routes

app.post('/api/v1/blog', (c) => { Copy


console.log(c.get('userId'));
return c.text('signin route')
})

Send the Header from Postman and ensure that the user id gets logged on the server

Callout

💡 If you want, you can extract the prisma variable in a global middleware that set’s it on
the context variable

app.use(”*”, (c) => { Copy


const prisma = new PrismaClient({
datasourceUrl: c.env.DATABASE_URL,
}).$extends(withAccelerate());
c.set(”prisma”, prisma);
})

Ref https://stackoverflow.com/questions/75554786/use-cloudflare-worker-env-outside-fetch-
scope

https://projects.100xdevs.com/pdf/blog/blog-1 11/29
7/8/24, 11:35 PM DailyCode

Step 7 - Blog routes and better


routing

Better routing
https://hono.dev/api/routing#grouping
Hono let’s you group routes together so you can have a cleaner file structure.

Create two new files -

routes/user.ts

routes/blog.ts
and push the user routes to user.ts
index.ts

import { Hono } from 'hono' Copy


import { userRouter } from './routes/user';
import { bookRouter } from './routes/blog';

export const app = new Hono<{


Bindings: {
DATABASE_URL: string;
JWT_SECRET: string;
}
}>();

app.route('/api/v1/user', userRouter)
app.route('/api/v1/book', bookRouter)

export default app

user.ts

import { PrismaClient } from "@prisma/client/edge"; Copy


import { withAccelerate } from "@prisma/extension-accelerate";
import { Hono } from "hono";
import { sign } from "hono/jwt";

export const userRouter = new Hono<{


Bindings: {
DATABASE_URL: string;
JWT_SECRET: string;
}
}>();

userRouter.post('/signup', async (c) => {

https://projects.100xdevs.com/pdf/blog/blog-1 12/29
7/8/24, 11:35 PM DailyCode

const prisma = new PrismaClient({


datasourceUrl: c.env.DATABASE_URL,
}).$extends(withAccelerate());

const body = await c.req.json();

const user = await prisma.user.create({


data: {
email: body.email,
password: body.password,
},
});

const token = await sign({ id: user.id }, c.env.JWT_SECRET)

return c.json({
jwt: token
})
})

userRouter.post('/signin', async (c) => {


const prisma = new PrismaClient({
//@ts-ignore
datasourceUrl: c.env?.DATABASE_URL ,
}).$extends(withAccelerate());

const body = await c.req.json();


const user = await prisma.user.findUnique({
where: {
email: body.email,
password: body.password
}
});

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

Blog routes

1. Create the route to initialize a blog/post


Solution

https://projects.100xdevs.com/pdf/blog/blog-1 13/29
7/8/24, 11:35 PM DailyCode

app.post('/', async (c) => { Copy


const userId = c.get('userId');
const prisma = new PrismaClient({
datasourceUrl: c.env?.DATABASE_URL ,
}).$extends(withAccelerate());

const body = await c.req.json();


const post = await prisma.post.create({
data: {
title: body.title,
content: body.content,
authorId: userId
}
});
return c.json({
id: post.id
});
})

2. Create the route to update blog


Solution

app.put('/api/v1/blog', async (c) => { Copy


const userId = c.get('userId');
const prisma = new PrismaClient({
datasourceUrl: c.env?.DATABASE_URL ,
}).$extends(withAccelerate());

const body = await c.req.json();


prisma.post.update({
where: {
id: body.id,
authorId: userId
},
data: {
title: body.title,
content: body.content
}
});

return c.text('updated post');


});

3. Create the route to get a blog


Solution

https://projects.100xdevs.com/pdf/blog/blog-1 14/29
7/8/24, 11:35 PM DailyCode

app.get('/api/v1/blog/:id', async (c) => { Copy


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

4. Create the route to get all blogs


Solution

app.get('/api/v1/blog/bulk', async (c) => { Copy


const prisma = new PrismaClient({
datasourceUrl: c.env?.DATABASE_URL ,
}).$extends(withAccelerate());

const posts = await prisma.post.find({});

return c.json(posts);
})

Try to hit the routes via POSTMAN and ensure they work as expected

https://projects.100xdevs.com/pdf/blog/blog-1 15/29
7/8/24, 11:35 PM DailyCode

Step 8 - Understanding the types

Bindings
https://hono.dev/getting-started/cloudflare-workers#bindings

In our case, we need 2 env variables -


JWT_SECRET
DATABASE_URL

https://projects.100xdevs.com/pdf/blog/blog-1 16/29
7/8/24, 11:35 PM DailyCode

Variables
https://hono.dev/api/context#var
If you wan’t to get and set values on the context of the request, you can use c.get and c.set

You need to make typescript aware of the variables that you will be setting on the context.

https://projects.100xdevs.com/pdf/blog/blog-1 17/29
7/8/24, 11:35 PM DailyCode

💡 You can also create a middleware that sets prisma in the context so you don’t need to
initialise it in the function body again and again

Step 9 - Deploy your app


npm run deploy Copy

💡 Make sure you have logged in the cloudflare cli using npx wrangler login

Update the env variables from cloudflare dashboard

Test your production URL in postman, make sure it works

https://projects.100xdevs.com/pdf/blog/blog-1 18/29
7/8/24, 11:35 PM DailyCode

Step 10 - Zod validation

https://projects.100xdevs.com/pdf/blog/blog-1 19/29
7/8/24, 11:35 PM DailyCode

If you’ve gone through the video Cohort 1 - Deploying npm packages, Intro to Monorepos ,
you’ll notice we introduced type inference in Zod
https://zod.dev/?id=type-inference

Step 11 - Initialise common


This let’s you get types from runtime zod variables that you can use on your frontend
1. Create a new folder called common and initialize an empty ts project in it

mkdir common Copy


cd common
npm init -y
npx tsc --init

1. Update tsconfig.json

"rootDir": "./src", Copy


"outDir": "./dist",
"declaration": true,

1. Sign up/login to npmjs.org

2. Run npm login

3. Update the name in package.json to be in your own npm namespace, Update main to be
dist/index.js

{ Copy
"name": "@100xdevs/common-app",
"version": "1.0.0",
We will"description":
divide our project
"",into 3 parts
"main": "dist/index.js",
1. Backend
"scripts": {
2. Frontend
"test": "echo \"Error: no test specified\" && exit 1"
},
3. common
"keywords": [],
will contain
"author":
common "",all the things that frontend and backend want to share.
We will"license":
make common an independent npm module for now.
"ISC"
Eventually, we will see how monorepos make it easier to have multiple packages sharing code in
}
the same repo
1. Add src to .npmignore

2. Install zod

npm i zod Copy

1. Put all types in src/index.ts

1. signupInput / SignupInput

2. signinInput / SigninInput

https://projects.100xdevs.com/pdf/blog/blog-1 20/29
7/8/24, 11:35 PM DailyCode

3. createPostInput / CreatePostInput

4. updatePostInput / UpdatePostInput

Solution

import z from "zod"; Copy

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

1. tsc -b to generate the output

2. Publish to npm

npm publish --access public Copy

1. Explore your package on npmjs

https://projects.100xdevs.com/pdf/blog/blog-1 21/29
7/8/24, 11:35 PM DailyCode

Step 12 - Import zod in backend


1. Go to the backend folder

cd backend Copy

1. Install the package you published to npm

npm i your_package_name Copy

1. Explore the package

cd node_modules/your_package_name Copy

1. Update the routes to do zod validation on them

Solution

import { PrismaClient } from '@prisma/client/edge' Copy


import { withAccelerate } from '@prisma/extension-accelerate'
import { Hono } from 'hono';
import { sign, verify } from 'hono/jwt'
import { signinInput, signupInput, createPostInput, updatePostInput

// 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" });
}

https://projects.100xdevs.com/pdf/blog/blog-1 22/29
7/8/24, 11:35 PM DailyCode

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

https://projects.100xdevs.com/pdf/blog/blog-1 23/29
7/8/24, 11:35 PM DailyCode

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

https://projects.100xdevs.com/pdf/blog/blog-1 24/29
7/8/24, 11:35 PM DailyCode

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;

Step 13 - Init the FE project


1. Initialise a react app

npm create vite@latest Copy

1. Initialise tailwind
https://tailwindcss.com/docs/guides/vite

npm install -D tailwindcss postcss autoprefixer Copy


npx tailwindcss init -p

1. Update tailwind.config.js

/** @type {import('tailwindcss').Config} */ Copy


export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",

https://projects.100xdevs.com/pdf/blog/blog-1 25/29
7/8/24, 11:35 PM DailyCode

],
theme: {
extend: {},
},
plugins: [],
}

1. Update index.css

@tailwind base; Copy


@tailwind components;
@tailwind utilities;

1. Empty up App.css

2. Install your package

npm i your_package Copy

1. Run the project locally

npm run dev Copy

Step 14 - Add react-router-dom


1. Add react-router-dom

npm i react-router-dom Copy

1. Add routing (ensure you create the Signup, Signin and Blog components)

import { BrowserRouter, Route, Routes } from 'react-router-dom' Copy


import { Signup } from './pages/Signup'
import { Signin } from './pages/Signin'
import { Blog } from './pages/Blog'

function App() {

https://projects.100xdevs.com/pdf/blog/blog-1 26/29
7/8/24, 11:35 PM DailyCode

return (
<>
<BrowserRouter>
<Routes>
<Route path="/signup" element={<Signup />} />
<Route path="/signin" element={<Signin />} />
<Route path="/blog/:id" element={<Blog />} />
</Routes>
</BrowserRouter>
</>
)
}

export default App

1. Make sure you can import types from your_package

Step 15 - Creating the components

https://projects.100xdevs.com/pdf/blog/blog-1 27/29
7/8/24, 11:35 PM DailyCode

Designs generated from v0.dev - an AI service by vercel that lets you generate frontends

Signup page

Blogs page

Create blog page

https://projects.100xdevs.com/pdf/blog/blog-1 28/29
7/8/24, 11:35 PM DailyCode

Blogs page

https://projects.100xdevs.com/pdf/blog/blog-1 29/29

You might also like