<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Rakesh Potnuru on Medium]]></title>
        <description><![CDATA[Stories by Rakesh Potnuru on Medium]]></description>
        <link>https://medium.com/@itsrakesh?source=rss-e09c62468ad2------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*aGxPlUtz3PBFPTPAt2YEkQ.png</url>
            <title>Stories by Rakesh Potnuru on Medium</title>
            <link>https://medium.com/@itsrakesh?source=rss-e09c62468ad2------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 08 Apr 2026 13:06:03 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@itsrakesh/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[AI Tools I Used This Month]]></title>
            <link>https://medium.com/@itsrakesh/ai-tools-i-used-this-month-abf44e21aa98?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/abf44e21aa98</guid>
            <category><![CDATA[vibe-coding]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[ai-tools]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Sun, 01 Feb 2026 10:36:17 GMT</pubDate>
            <atom:updated>2026-02-01T10:36:17.581Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bNbC95UGXjHvyIHbGuxc6Q.png" /></figure><p>There’s so much going on in the AI space, and it’s hard to focus on what truly matters. AI tools, platforms, and frameworks are popping up every day like JavaScript frameworks used to be. So I’ll try to keep track with an article every month.</p><h3>Cline</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Hzgvp20AbgxFgRrn0V_xVw.png" /><figcaption>Cline</figcaption></figure><p>Vibe code platforms are cool, but they have their limits. They can take you from zero to MVP in minutes. Once you realise that vibe coding is no longer reliable to take your project further, or don’t want to spend anymore $$$, you have to take matters into your own hands.</p><p>That’s why I prefer Agent-driven development over vibe-coding, and that’s exactly what Cline is for.</p><p><a href="https://cline.bot/learn">Cline</a> lets you define a project brief, tech stack, system patterns (code style, best practices, etc), and so much more in a dedicated folder called “Memory bank”. The best thing about the memory bank is that it works with any LLM, so you are not forced to use Cline. Switch between different LLMs without writing dedicated rules for each.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GB2tF5M4LkW0xnQQ9zjg2Q.png" /><figcaption>cline memory bank</figcaption></figure><p>Not only this, you can automate different parts of your dev process with Cline workflows. For example, every time you implement a new feature, you have to go through different steps: 1. commit changes, 2. push to github 3. create a release, 4. build &amp; submit to app store, 5. update your to-do list, etc. You can automate all this by defining a single .mdfile and triggering it with /your-workflow.md . So you don’t have to remember multiple commands.</p><p>There’s a lot more. Checkout their docs.</p><h3>Kestra</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4KUqElTBH4ckDwmrO3ZPsA.png" /><figcaption>Kestra</figcaption></figure><p>Ever wanted to build AI agents to automate your workflows or just do some cool stuff? You can do so with Kestra without writing a single line of code.</p><p>While <a href="https://kestra.io/docs/ai-tools/ai-agents">Kestra</a> is not an “AI Tool”, you can use their orchestration workflows to build powerful AI agents. I built a research agent that takes a topic -&gt; searches the web -&gt; divides tasks to several sub-agents -&gt; gives me a clean research brief and updates my Supabase database after each step. All without touching the code. You can check it out <a href="https://github.com/RakeshPotnuru/astralyte">here</a>.</p><h3>Coderabbit</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d3i8yzkak00ISUVHAqVStw.png" /><figcaption>Coderabbit</figcaption></figure><p>Code reviews are one of the most time-consuming, energy-draining tasks in software engineering. Now with AI <em>s̶l̶o̶p̶</em> generated code, it’s harder than ever.</p><p><a href="https://www.coderabbit.ai/">Coderabbit</a> reviews your code, flags harmful code, suggests changes, creates summaries, and even generates nice sequence diagrams like below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4bZxgNzTOPMG53OppgvRYA.png" /></figure><h3>Vibe Coding</h3><p>Vibe coding is no longer what it used to be. I’m genuinely shocked when I first tried <a href="https://aistudio.google.com/apps">Google AI Studio</a>. I literally built a tiny AI game in a few minutes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xPZRKK1RLa7vION3pFsqIg.png" /><figcaption>Google AI Studio</figcaption></figure><p>I thought it couldn’t get any better, then I tried <a href="https://rork.com">Rork</a>. I built an entire mobile app in a few hours with Expo + React Native, including backend, payments, db, and even published to App Store Testflight.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NJNV1fFIsOpAgPRlv6iXkA.png" /><figcaption>Rork</figcaption></figure><p>It’s truly mind-blowing how far we have come from AI tab completions to building apps in a few minutes.</p><p>Don’t sleep on AI and follow me to stay updated. Onwards &amp; upwards 🚀.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=abf44e21aa98" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Integrating DeepSeek API in NextJs and ExpressJs App]]></title>
            <link>https://blog.devgenius.io/integrating-deepseek-api-in-nextjs-and-expressjs-app-e6234ac3c2bc?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/e6234ac3c2bc</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[deepseek]]></category>
            <category><![CDATA[nextjs]]></category>
            <category><![CDATA[expressjs]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Wed, 12 Feb 2025 18:53:26 GMT</pubDate>
            <atom:updated>2025-02-13T17:00:28.006Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ybryF0U4L5n_WbrUVdyXgQ.png" /><figcaption>Integrating DeepSeek API in NextJs and ExpressJs App</figcaption></figure><p><em>Published from </em><a href="https://publishstudio.one"><em>Publish Studio</em></a></p><p>AI is an infinity stone. Learn to use its power, you can do wonders. In this guide, build a personal accountant with DeepSeek API and a sample project.</p><figure><img alt="ai personal accountant" src="https://cdn-images-1.medium.com/max/1024/0*1CBoXYp1dbBaKg3V.gif" /></figure><h4>The Project</h4><p>I’ve created a sample project called “Finance Tracker” for this tutorial. It lets you record your financial transactions. The front end is built with NextJs and the back end is with tRPC (express adapter) and Postgres database with drizzle ORM. But you don’t need to know tRPC to continue with this tutorial. (In case you want to learn tRPC, check out <a href="https://itsrakesh.com/blog/series/build-a-full-stack-app-with-trpc-and-next-js">Build a Full-Stack App with tRPC and Next.js App Router</a> series.)</p><p>Github repo: <a href="https://github.com/itsrakeshhq/finance-tracker">https://github.com/itsrakeshhq/finance-tracker</a></p><p>Let’s add a chatbot to the product that acts as our personal accountant.</p><h4>Backend</h4><h4>Getting DeepSeek API key</h4><p>As you might already know, <a href="https://www.deepseek.com">DeepSeek</a> went pretty viral when it was launched because of comparable performance to OpenAI yet only a fraction of the cost. This resulted in massive downtimes since its launch. I’ve been trying to use their API for a long time but no luck. (<em>If you can get it, then please continue with it.</em>)</p><p>So instead of using API from the <a href="https://platform.deepseek.com/">DeepSeek API platform</a> directly, we can use it from OpenRouter. <a href="https://openrouter.ai">OpenRouter</a> gives access to various AI models from various providers. So if one provider goes down, it switches to another one.</p><h4>API Integration</h4><p>Put the API key you got from above in backend/.env:</p><pre>DEEPSEEK_API_KEY=your_deepseek_api_key</pre><p>Since DeepSeek API is compatible with OpenAI SDK, let’s install that:</p><pre>yarn add openai</pre><p>Create src/modules/ai/ai.controller.ts. This is where will write AI accountant code. First, create OpenAI client:</p><pre>export default class AiController {<br>  private readonly openai: OpenAI;<br><br>  constructor() {<br>    this.openai = new OpenAI({<br>      // baseURL: &quot;https://api.deepseek.com&quot; // if using DeepSeek API key<br>      baseURL: &quot;https://openrouter.ai/api/v1&quot;,<br>      apiKey: process.env.DEEPSEEK_API_KEY,<br>    });<br>  }<br>}</pre><p>Here we’ve initialized Openai client.</p><blockquote>Note: Make sure to use appropriate baseURL and apiKey based on what you are using - DeepSeek or OpenRouter.</blockquote><p>Here’s how I designed the accountant:</p><ol><li>Fetch transactions from DB.</li><li>Include in the prompt.</li><li>Answer user queries based on transactions.</li><li>Stream response.</li></ol><pre>...<br>  async accountant(req: Request, res: Response) {<br>    try {<br>      const { query } = req.body;<br><br>      if (!query) {<br>        return res.status(400).json({ error: &quot;Query is required&quot; });<br>      }<br><br>      const userId = req.user.id;<br><br>      const data = await db<br>        .select()<br>        .from(transactions)<br>        .where(eq(transactions.userId, userId));<br><br>      if (data.length === 0) {<br>        return res.status(400).json({ error: &quot;No transactions found&quot; });<br>      }<br><br>      const formattedTxns = data.map((txn) =&gt; ({<br>        amount: txn.amount,<br>        txnType: txn.txnType,<br>        summary: txn.summary,<br>        tag: txn.tag,<br>        date: txn.createdAt,<br>      }));<br><br>      const prompt = `<br>    You are a personal accountant. You are given a list of transactions. You need to answer user query based on the transactions. <br><br>    YOU MUST KEEP THESE POINTS IN MIND WHILE ANSWERING THE USER QUERY:<br>    1. You must give straight forward answer.<br>    2. Answer like you are talking to the user. <br>    3. You must not output your thinking process and reasoning. This is very important.<br>    4. Answer should be in markdown format.<br><br>    Transactions: ${JSON.stringify(formattedTxns)}<br>    Currency: $ (USD)<br><br>    User Query: ${query}<br>    `;<br><br>      const response = await this.openai.chat.completions.create({<br>        model: &quot;deepseek/deepseek-chat&quot;,<br>        messages: [{ role: &quot;user&quot;, content: prompt }],<br>        stream: true,<br>      });<br><br>      res.writeHead(200, {<br>        &quot;Content-Type&quot;: &quot;text/plain&quot;,<br>        &quot;transfer-encoding&quot;: &quot;chunked&quot;,<br>      });<br><br>      for await (const chunk of response) {<br>        if (chunk.choices[0].finish_reason === &quot;stop&quot;) {<br>          break;<br>        }<br><br>        res.write(chunk.choices[0].delta.content || &quot;&quot;);<br>      }<br><br>      res.end();<br>    } catch (error) {<br>      console.error({ error });<br>      if (!res.headersSent) {<br>        res.status(500).json({ error: &quot;Internal server error&quot; });<br>      }<br>    }<br>  }<br>...</pre><blockquote>Note: This is just to give an idea. In real projects it’s not ideal to fetch all transactions from db for every query. To provide context to AI models, you can create <a href="https://aws.amazon.com/what-is/embeddings-in-machine-learning/">embeddings</a>. DeepSeek currently does not support embeddings.</blockquote><p>As you can see all we did was write a prompt and provide as much context as possible to get accurate results. Then we are sending the response as a stream instead of waiting for the whole response which might take a lot of time.</p><p>And then expose this controller in a route:</p><pre>// src/index.ts<br><br>...<br>app.use(express.json());<br>app.post(&quot;/ai&quot;, authMiddleware, (req, res) =&gt;<br>  new AiController().accountant(req, res)<br>);<br>...</pre><p>authMiddleware protects the endpoint, so only logged-in users can access it. You can find the implementation in src/middleware/auth-middleware.ts.</p><p>That’s all we need from the backend.</p><h4>Frontend</h4><p>In the front end, let’s create a chat widget. Switch to frontend/ folder.</p><p>Create chat.tsx in src/components/modules/dashboard.</p><h4>Chat UI</h4><p>Then create a chat box UI with shadcn popover component.</p><pre>// chat.tsx<br><br>export default function Chat() {<br>  const [conversation, setConversation] = useState&lt;<br>    {<br>      role: &quot;user&quot; | &quot;assistant&quot;;<br>      content: string;<br>    }[]<br>  &gt;([<br>    {<br>      role: &quot;assistant&quot;,<br>      content: &quot;Hello, how can I help you today?&quot;,<br>    },<br>  ]);<br>  const [liveResponse, setLiveResponse] = useState&lt;string&gt;(&quot;&quot;);<br>  const [isThinking, setIsThinking] = useState&lt;boolean&gt;(false);<br>  <br>  // Auto scroll to bottom when new message is added<br>  const scrollRef = useRef&lt;HTMLDivElement&gt;(null);<br>  useEffect(() =&gt; {<br>    if (scrollRef.current) {<br>      scrollRef.current.scrollIntoView({ behavior: &quot;smooth&quot; });<br>    }<br>  }, [conversation, liveResponse]);<br>  <br>  return (<br>	  &lt;Popover&gt;<br>      &lt;PopoverTrigger className=&quot;absolute right-4 bottom-4&quot; asChild&gt;<br>        &lt;Button size={&quot;icon&quot;} className=&quot;rounded-full&quot;&gt;<br>          &lt;BotMessageSquareIcon className=&quot;w-4 h-4&quot; /&gt;<br>        &lt;/Button&gt;<br>      &lt;/PopoverTrigger&gt;<br>      &lt;PopoverContent align=&quot;end&quot; className=&quot;w-[500px] h-[600px] p-0 space-y-4&quot;&gt;<br>        &lt;h1 className=&quot;text-xl font-bold text-center p-4 pb-0&quot;&gt;<br>          Personal Accountant<br>        &lt;/h1&gt;<br>        &lt;hr /&gt;<br>        &lt;div className=&quot;pt-0 relative h-full&quot;&gt;<br>          &lt;div className=&quot;flex flex-col gap-2 h-[calc(100%-150px)] overflow-y-auto px-4 pb-20&quot;&gt;<br>            {conversation.map((message, index) =&gt; (<br>              &lt;div<br>                key={index}<br>                className={cn(&quot;flex flex-row gap-2 items-start&quot;, {<br>                  &quot;rounded-lg bg-muted p-2 ml-auto flex-row-reverse&quot;:<br>                    message.role === &quot;user&quot;,<br>                })}<br>              &gt;<br>                {message.role === &quot;assistant&quot; &amp;&amp; (<br>                  &lt;BotMessageSquareIcon className=&quot;w-4 h-4 shrink-0 mt-1.5&quot; /&gt;<br>                )}<br>                {message.role === &quot;user&quot; &amp;&amp; (<br>                  &lt;UserRoundIcon className=&quot;w-4 h-4 shrink-0 mt-1&quot; /&gt;<br>                )}<br>                &lt;Markdown className=&quot;prose prose-sm prose-h1:text-xl prose-h2:text-lg prose-h3:text-base&quot;&gt;<br>                  {message.content}<br>                &lt;/Markdown&gt;<br>              &lt;/div&gt;<br>            ))}<br>            {isThinking &amp;&amp; (<br>              &lt;div className=&quot;flex flex-row gap-2 items-center&quot;&gt;<br>                &lt;BotMessageSquareIcon className=&quot;w-4 h-4&quot; /&gt;<br>                &lt;p className=&quot;animate-pulse prose prose-sm&quot;&gt;Thinking...&lt;/p&gt;<br>              &lt;/div&gt;<br>            )}<br>            {liveResponse.length &gt; 0 &amp;&amp; (<br>              &lt;div className=&quot;flex flex-row gap-2 items-start&quot;&gt;<br>                &lt;BotMessageSquareIcon className=&quot;w-4 h-4 shrink-0 mt-1.5&quot; /&gt;<br>                &lt;Markdown className=&quot;prose prose-sm prose-h1:text-xl prose-h2:text-lg prose-h3:text-base&quot;&gt;<br>                  {liveResponse}<br>                &lt;/Markdown&gt;<br>              &lt;/div&gt;<br>            )}<br>            &lt;div ref={scrollRef} /&gt;<br>          &lt;/div&gt;<br>          &lt;hr /&gt;<br>         &lt;/div&gt;<br>      &lt;/PopoverContent&gt;<br>    &lt;/Popover&gt;<br>  );<br>}</pre><h4>Form</h4><p>Now create a form to handle user input:</p><pre>const formSchema = z.object({<br>  message: z.string().min(1),<br>});<br><br>export default function Chat() {<br>...<br>  const form = useForm&lt;z.infer&lt;typeof formSchema&gt;&gt;({<br>    resolver: zodResolver(formSchema),<br>    defaultValues: {<br>      message: &quot;&quot;,<br>    },<br>  });<br>  <br>  ...<br>      &lt;hr /&gt;<br>          &lt;div className=&quot;absolute bottom-20 left-0 right-0 p-4 w-full&quot;&gt;<br>            &lt;Form {...form}&gt;<br>              &lt;form<br>                onSubmit={form.handleSubmit(onSubmit)}<br>                className=&quot;flex flex-row gap-2&quot;<br>              &gt;<br>                &lt;FormField<br>                  control={form.control}<br>                  name=&quot;message&quot;<br>                  render={({ field }) =&gt; (<br>                    &lt;FormItem className=&quot;flex-1&quot;&gt;<br>                      &lt;FormControl&gt;<br>                        &lt;Input placeholder=&quot;Ask me anything...&quot; {...field} /&gt;<br>                      &lt;/FormControl&gt;<br>                      &lt;FormMessage /&gt;<br>                    &lt;/FormItem&gt;<br>                  )}<br>                /&gt;<br>                &lt;Button size={&quot;icon&quot;} type=&quot;submit&quot;&gt;<br>                  &lt;SendIcon className=&quot;w-4 h-4&quot; /&gt;<br>                &lt;/Button&gt;<br>              &lt;/form&gt;<br>            &lt;/Form&gt;<br>          &lt;/div&gt;<br>    ...<br>...</pre><h4>Submit handler</h4><p>Finally, implement the submit handler. As I said above, we are sending responses in stream, so it’s better if also show the response as it is coming.</p><pre>...<br>  const onSubmit = async (data: z.infer&lt;typeof formSchema&gt;) =&gt; {<br>    setIsThinking(true);<br>    setConversation((prev) =&gt; [<br>      ...prev,<br>      { role: &quot;user&quot;, content: data.message },<br>    ]);<br>    form.reset();<br><br>    let liveResponse = &quot;&quot;;<br>    try {<br>      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/ai`, {<br>        method: &quot;POST&quot;,<br>        headers: {<br>          &quot;Content-Type&quot;: &quot;application/json&quot;,<br>        },<br>        credentials: &quot;include&quot;,<br>        body: JSON.stringify({<br>          query: data.message,<br>        }),<br>      });<br><br>      const reader = response.body?.getReader();<br>      const decoder = new TextDecoder();<br><br>      setIsThinking(false);<br>      if (!reader) return;<br><br>      while (true) {<br>        const { done, value } = await reader.read();<br>        if (done) break;<br><br>        const text = decoder.decode(value, { stream: true });<br><br>        liveResponse += text;<br>        setLiveResponse(liveResponse);<br>      }<br><br>      setConversation((prev) =&gt; [<br>        ...prev,<br>        { role: &quot;assistant&quot;, content: liveResponse },<br>      ]);<br>    } catch (error) {<br>      console.error(error);<br>    } finally {<br>      setIsThinking(false);<br>      setLiveResponse(&quot;&quot;);<br>    }<br>  };<br>...</pre><blockquote>Note: Make sure to set NEXT_PUBLIC_API_URL in .env.local</blockquote><p>That’s it!</p><p>If you don’t want to deal with all this, you can also use Vercel’s <a href="https://sdk.vercel.ai">AI SDK</a> which has all the parts you need.</p><p>Feel free to ask any questions in the comments below. Follow for more 🎸🎸🎸!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e6234ac3c2bc" width="1" height="1" alt=""><hr><p><a href="https://blog.devgenius.io/integrating-deepseek-api-in-nextjs-and-expressjs-app-e6234ac3c2bc">Integrating DeepSeek API in NextJs and ExpressJs App</a> was originally published in <a href="https://blog.devgenius.io">Dev Genius</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Email Verification System in Next.js and tRPC with Resend]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/email-verification-system-in-next-js-and-trpc-with-resend-a19515817934?source=rss-e09c62468ad2------2"><img src="https://cdn-images-1.medium.com/max/1600/1*rED4gurgwbwzI4ySUNqr4A.png" width="1600"></a></p><p class="medium-feed-link"><a href="https://medium.com/codex/email-verification-system-in-next-js-and-trpc-with-resend-a19515817934?source=rss-e09c62468ad2------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/email-verification-system-in-next-js-and-trpc-with-resend-a19515817934?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/a19515817934</guid>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Mon, 03 Feb 2025 19:17:34 GMT</pubDate>
            <atom:updated>2025-02-05T17:27:31.335Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Cookie-Based Authentication in Nextjs App Router Application]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/cookie-based-authentication-in-nextjs-app-router-application-73d97e9fabc3?source=rss-e09c62468ad2------2"><img src="https://cdn-images-1.medium.com/max/1600/1*cIemNoZGQkGM_2RtmgW8XQ.png" width="1600"></a></p><p class="medium-feed-link"><a href="https://medium.com/codex/cookie-based-authentication-in-nextjs-app-router-application-73d97e9fabc3?source=rss-e09c62468ad2------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/cookie-based-authentication-in-nextjs-app-router-application-73d97e9fabc3?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/73d97e9fabc3</guid>
            <category><![CDATA[trpc]]></category>
            <category><![CDATA[authentication]]></category>
            <category><![CDATA[nextjs]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Sat, 25 Jan 2025 12:24:49 GMT</pubDate>
            <atom:updated>2025-01-29T15:59:35.258Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Implementing Cookie-Based JWT Authentication in a tRPC Backend]]></title>
            <link>https://medium.com/codex/implementing-cookie-based-jwt-authentication-in-a-trpc-backend-6b6c1b2710b0?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/6b6c1b2710b0</guid>
            <category><![CDATA[auth]]></category>
            <category><![CDATA[jwt]]></category>
            <category><![CDATA[trpc]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Thu, 16 Jan 2025 18:57:04 GMT</pubDate>
            <atom:updated>2025-01-27T17:12:33.238Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b2-4EgV75JYrhG3aMtqGjA.png" /><figcaption>Implementing Cookie-Based JWT Authentication in a tRPC Backend</figcaption></figure><p><em>Published from </em><a href="https://publishstudio.one"><em>Publish Studio</em></a></p><p>Authentication is an important part of a full-stack app. This is the only thing left before you can call yourself a full-stack developer or engineer or whatever. So, in this article, I will share how to add classic (email + password) authentication to a full-stack app using Next.js middleware for the front end and tRPC for the back end.</p><p>For the sake of learning I’m not going to use third-party auth solutions like Auth.js, Clerk, auth0, Supabase auth. But in real apps it’s better to use an auth solution because they handle everything for you and they are more secure.</p><p>This is part 3 of “<a href="https://itsrakesh.com/blog/series/build-a-full-stack-app-with-trpc-and-next-js">Building a Full-Stack App with tRPC and Next.js</a>” series. I recommended reading first 2 parts if you haven’t to better understand this part.</p><p>Let’s say we want to allow others to use our product Finance Tracker <a href="https://github.com/itsrakeshhq/finance-tracker">(GitHub repo)</a>. Before giving them access to the product, we have to do something, so they can’t access each other’s data and keep their info secure. This is where auth comes in.</p><p>To achieve this goal, we have to let them create an account in our server and verify every time someone makes a request like adding a transaction so that we add that transaction to that specific user account.</p><p>Two important terms to understand before we move on:</p><ol><li><strong>Authentication</strong>: Letting the user into the server (through login).</li><li><strong>Authorization</strong>: Giving permission to the user to perform certain actions (e.g.: add transaction, view transactions).</li></ol><h4>The Concept of Access and Refresh Tokens</h4><p>Access tokens, as the name implies used to access resources from server. They often set to expire within 10 minutes to 1 hour. While refresh tokens are used only to get new access token after previous one expires and they often are long-lived (mostly 7+ days).</p><p><strong>Then why do we need refresh tokens and why not make access tokens long-lived?</strong></p><p>The whole point of refresh tokens is to <strong>minimize the attack window</strong>. Since access tokens are sent frequently, they have higher risk of getting compromised (like man-in-the middle attacks) than refresh tokens. And as they are short-lived, they reduce damage.</p><p>But if you don’t use refresh tokens, you have to ask the user to log in again and again which is a bad user experience.</p><h4>Securing Backend</h4><p>Before adding auth, we have to create a user model to create accounts and create a relation with transactions.</p><p>So, open backend/src/modules and create user module and user.schema.ts file. Then create basic user schema along with zod schema for insert operation:</p><pre>import { pgTable, serial, text, timestamp } from &quot;drizzle-orm/pg-core&quot;;<br>import { createInsertSchema } from &quot;drizzle-zod&quot;;<br><br>export const users = pgTable(&quot;users&quot;, {<br>  id: serial(&quot;id&quot;).primaryKey(),<br>  email: text(&quot;email&quot;).notNull().unique(),<br>  password: text(&quot;password&quot;).notNull(),<br>  createdAt: timestamp(&quot;created_at&quot;).defaultNow(),<br>  updatedAt: timestamp(&quot;updated_at&quot;)<br>    .defaultNow()<br>    .$onUpdate(() =&gt; new Date()),<br>});<br><br>export const insertUserSchema = createInsertSchema(users).omit({<br>  id: true,<br>  createdAt: true,<br>  updatedAt: true,<br>});</pre><p>Next, create a relation between the transaction and the user. Since one user can have many transactions, let’s create one-to-many a relation.</p><p>In user.schema.ts:</p><pre>import { relations } from &quot;drizzle-orm&quot;;<br><br>export const usersRelations = relations(users, ({ many }) =&gt; ({<br>// each user can have multiple transactions<br>  transactions: many(transactions),<br>}));</pre><p>In transaction.schema.ts:</p><pre>export const transactions = pgTable(&quot;transactions&quot;, {<br>...<br>	userId: integer(&quot;user_id&quot;) // &lt;--- add userId<br>	    .references(() =&gt; users.id, { onDelete: &quot;cascade&quot; }) // &lt;--- delete transaction when referenced user is deleted<br>	    .notNull(), <br>...<br>});<br><br>export const transactionsRelations = relations(transactions, ({ one }) =&gt; ({<br>// each transaction belongs to only one user<br>  user: one(users, {<br>    fields: [transactions.userId],<br>    references: [users.id],<br>  }),<br>}));<br><br>export const insertTransactionSchema = createInsertSchema(transactions).omit({<br>  ...<br>  userId: true, // &lt;--- Remove userId from zod schema because we will get that from auth context which will be explained later<br>});</pre><p>Now run migrations:</p><pre>npx drizzle-kit push</pre><p>You will get a warning (THIS ACTION WILL CAUSE DATA LOSS AND CANNOT BE REVERTED) because we are adding a required field user_id but we have some data from the previous tutorial. Since it&#39;s just a tutorial, select truncate data.</p><p>Alright, it’s time to add authentication.</p><p>First, install</p><ol><li>bcryptjs - to hash password</li><li>jsonwebtoken - to generate access and refresh tokens</li><li>cookies - to store tokens in secure cookies and include them in response when logging in so they can be stored in the user&#39;s browser to keep them logged in from the client side</li><li>ioredis - to store the user ID and refresh token in Redis to keep users logged in from the server side and identify them later when requesting resources or new access token</li></ol><pre>yarn add bcryptjs jsonwebtoken cookies ioredis</pre><p>Here’s the flow of authentication:</p><figure><img alt="auth flow" src="https://cdn-images-1.medium.com/max/1024/0*lmo2YWc4eK9DKP3O.png" /></figure><p><strong>Why store refresh tokens in Redis? Why not just store the user and then decode the user from the refresh token?</strong></p><p>Let’s say a user is logged in from two devices, if you only store user, you create a single session. When one refresh token is compromised and you want to invalidate that, you have to logout user from all devices.</p><p>But if you store refresh token, user can have multiple sessions and you can have fine-grained control over user sessions and avoid misuse of the server resources. And also let user know how many sessions they currently have and show session info like device and location. This is why you see helpful email notifications like “Your account has been accessed from a new ip”.</p><p>If you don’t want advanced session management then a single session with just user data is enough.</p><h4>Set up Redis</h4><p>Create src/utils/redis.ts file. Configure Redis and create a Redis client:</p><pre>import { Redis } from &quot;ioredis&quot;;<br><br>export const redis = new Redis(process.env.REDIS_URL!);</pre><p>Make sure to add REDIS_URL to env. If using <a href="https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/">local Redis</a>, the URL looks like this:</p><pre>REDIS_URL=redis://localhost:6379</pre><h4>Creating, and verifying tokens</h4><p>Create another module called auth. In there, create auth.service.ts. Here, we will write reusable functions and generate and verify tokens:</p><pre>import jwt from &quot;jsonwebtoken&quot;;<br><br>const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;<br>const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;<br><br>export default class AuthService {<br>  createAccessToken(userId: number) {<br>    const accessToken = jwt.sign({ sub: userId }, ACCESS_TOKEN_SECRET, {<br>      expiresIn: &quot;15m&quot;,<br>    });<br><br>    return accessToken;<br>  }<br><br>  createRefreshToken(userId: number) {<br>    const refreshToken = jwt.sign({ sub: userId }, REFRESH_TOKEN_SECRET, {<br>      expiresIn: &quot;7d&quot;,<br>    });<br><br>    return refreshToken;<br>  }<br><br>  verifyAccessToken(accessToken: string) {<br>    try {<br>      const decoded = jwt.verify(accessToken, ACCESS_TOKEN_SECRET) as {<br>        sub: string;<br>      };<br><br>      return decoded.sub;<br>    } catch (error) {<br>      console.log(error);<br><br>      return null;<br>    }<br>  }<br><br>  verifyRefreshToken(refreshToken: string) {<br>    try {<br>      const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as {<br>        sub: string;<br>      };<br><br>      return decoded.sub;<br>    } catch (error) {<br>      console.log(error);<br><br>      return null;<br>    }<br>  }</pre><p>As you can see, we are creating tokens by signing them with a secret and then we use that same secret to verify them.</p><p>Open .env and create two new env variables - ACCESS_TOKEN_SECRET and REFRESH_TOKEN_SECRET. Secrets can be any strings but for real projects I recommend using RSA keys.</p><h4>Implement login</h4><p>Steps in login:</p><ol><li>Find the user in the db with email.</li><li>Verify if the password is correct by comparing it against the password in the db using bcrypt (since we store hashed passwords).</li><li>If all good, generate access and refresh tokens, store refresh token along with user id in Redis and return tokens.</li><li>Then in the user controller class, call this method and send the tokens in response as HTTP cookies.</li></ol><pre>// user.service.ts<br>import { db } from &quot;../../utils/db&quot;;<br>import { redis } from &quot;../../utils/redis&quot;;<br>import { users } from &quot;../user/user.schema&quot;;<br>import bcrypt from &quot;bcryptjs&quot;;<br>import { eq } from &quot;drizzle-orm&quot;;<br><br>export default class AuthService {<br>...<br>  async login(data: typeof users.$inferInsert) {<br>    const { email, password } = data;<br><br>    try {<br>      const user = (<br>        await db.select().from(users).where(eq(users.email, email)).limit(1)<br>      )[0];<br><br>      if (!user) {<br>        throw new TRPCError({<br>          code: &quot;UNAUTHORIZED&quot;,<br>          message: &quot;Invalid email or password&quot;,<br>        });<br>      }<br><br>      const isPasswordCorrect = await bcrypt.compare(password, user.password);<br><br>      if (!isPasswordCorrect) {<br>        throw new TRPCError({<br>          code: &quot;UNAUTHORIZED&quot;,<br>          message: &quot;Invalid email or password&quot;,<br>        });<br>      }<br><br>      const accessToken = this.createAccessToken(user.id);<br>      const refreshToken = this.createRefreshToken(user.id);<br><br>      // Store refresh token in redis to track active sessions<br>      await redis.set(<br>        `refresh_token:${refreshToken}`,<br>        user.id,<br>        &quot;EX&quot;,<br>        7 * 24 * 60 * 60 // 7 days<br>      );<br>      <br>      // Store refresh token in redis set to track active sessions<br>      await redis.sadd(`refresh_tokens:${user.id}`, refreshToken);<br>      await redis.expire(`refresh_tokens:${user.id}`, 7 * 24 * 60 * 60); // 7 days<br><br>      // Store user in redis to validate session<br>      await redis.set(<br>        `user:${user.id}`,<br>        JSON.stringify(user),<br>        &quot;EX&quot;,<br>        7 * 24 * 60 * 60<br>      ); // 7 days<br>      <br>      return {<br>        accessToken,<br>        refreshToken,<br>      };<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;UNAUTHORIZED&quot;,<br>        message: &quot;Something went wrong&quot;,<br>      });<br>    }<br>  }<br>...<br>}</pre><p>Create user.controller.ts:</p><pre>// user.controller.ts<br><br>import Cookies, { SetOption } from &quot;cookies&quot;;<br>import { Context } from &quot;../../trpc&quot;;<br>import { users } from &quot;../user/user.schema&quot;;<br>import AuthService from &quot;./auth.service&quot;;<br><br>const cookieOptions: SetOption = {<br>  httpOnly: true,<br>  secure: process.env.NODE_ENV === &quot;production&quot;,<br>  sameSite: &quot;strict&quot;,<br>  path: &quot;/&quot;,<br>  domain: &quot;localhost&quot; // for production, put something like &quot;.example.com&quot;<br>};<br><br>const accessTokenCookieOptions: SetOption = {<br>  ...cookieOptions,<br>  maxAge: 15 * 60 * 1000, // 15 minutes<br>};<br><br>const refreshTokenCookieOptions: SetOption = {<br>  ...cookieOptions,<br>  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days<br>};<br><br>export default class AuthController extends AuthService {<br>  async loginHandler(data: typeof users.$inferInsert, ctx: Context) {<br>    const { accessToken, refreshToken } = await super.login(data);<br><br>    const cookies = new Cookies(ctx.req, ctx.res, {<br>      secure: process.env.NODE_ENV === &quot;production&quot;,<br>    });<br>    cookies.set(&quot;accessToken&quot;, accessToken, { ...accessTokenCookieOptions });<br>    cookies.set(&quot;refreshToken&quot;, refreshToken, {<br>      ...refreshTokenCookieOptions,<br>    });<br>    cookies.set(&quot;logged_in&quot;, &quot;true&quot;, { ...accessTokenCookieOptions });<br><br>    return { success: true };<br>  }<br>}</pre><p>Create auth.routes.ts file:</p><pre>import { publicProcedure, router } from &quot;../../trpc&quot;;<br>import { insertUserSchema } from &quot;../user/user.schema&quot;;<br>import AuthController from &quot;./auth.controller&quot;;<br><br>const authRouter = router({<br>  login: publicProcedure<br>    .input(insertUserSchema)<br>    .mutation(({ input, ctx }) =&gt;<br>      new AuthController().loginHandler(input, ctx)<br>    ),<br>});<br><br>export default authRouter;</pre><p>Add this auth route group to src/routes.ts.</p><pre>import authRouter from &quot;./modules/auth/auth.routes&quot;;<br><br>const appRouter = router({<br>  ...<br>  auth: authRouter,<br>});</pre><h4>Implement register</h4><p>Register is simple, we just have to create an account. (I’m going to cover email verification in a later article since a lot of products ask users to verify email after giving access to the platform as part of their marketing strategy.)</p><p>Steps:</p><ol><li>Check if the user already exists.</li><li>If not, hash the password and create a user.</li></ol><pre>// auth.service.ts<br><br>...<br>  async register(data: typeof users.$inferInsert) {<br>    try {<br>      const { email, password } = data;<br><br>      const user = (<br>        await db.select().from(users).where(eq(users.email, email)).limit(1)<br>      )[0];<br>      if (user) {<br>        throw new TRPCError({<br>          code: &quot;CONFLICT&quot;,<br>          message:<br>            &quot;This email is associated with an existing account. Please login instead.&quot;,<br>        });<br>      }<br><br>      const salt = await bcrypt.genSalt(12);<br>      const hashedPassword = await bcrypt.hash(password, salt);<br><br>      const newUser = await db<br>        .insert(users)<br>        .values({<br>          email,<br>          password: hashedPassword,<br>        })<br>        .returning();<br><br>      return {<br>        success: true,<br>        user: newUser,<br>      };<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>        message: &quot;Something went wrong&quot;,<br>      });<br>    }<br>  }<br>...</pre><pre>// auth.controller.ts<br><br>...<br>  async registerHandler(data: typeof users.$inferInsert) {<br>    return await super.register(data);<br>  }<br>...</pre><pre>// auth.routes.ts<br><br>...<br>  register: publicProcedure<br>    .input(insertUserSchema)<br>    .mutation(({ input }) =&gt; new AuthController().registerHandler(input)),<br>...</pre><h4>Implement access token refresh</h4><p>To implement access token refresh:</p><ol><li>Check if a refresh token exists in active sessions.</li><li>Verify token.</li><li>Check if the user still exists.</li><li>If allis good, generate a new access token and send it as a cookie.</li></ol><pre>// auth.service.ts<br><br>...<br>  async refreshAccessToken(refreshToken: string) {<br>    try {<br>      const isTokenExist = await redis.get(`refresh_token:${refreshToken}`);<br>      if (!isTokenExist) {<br>        throw new TRPCError({<br>          code: &quot;UNAUTHORIZED&quot;,<br>          message: &quot;Invalid refresh token&quot;,<br>        });<br>      }<br><br>      const userId = await this.verifyRefreshToken(refreshToken);<br>      if (!userId) {<br>        throw new TRPCError({<br>          code: &quot;UNAUTHORIZED&quot;,<br>          message: &quot;Invalid refresh token&quot;,<br>        });<br>      }<br><br>      const accessToken = this.createAccessToken(parseInt(userId));<br><br>      return accessToken;<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>        message: &quot;Something went wrong&quot;,<br>      });<br>    }<br>  }<br>...</pre><pre>// auth.controller.ts<br><br>...<br>  async refreshAccessTokenHandler(ctx: AuthenticatedContext) {<br>    const cookies = new Cookies(ctx.req, ctx.res, {<br>      secure: process.env.NODE_ENV === &quot;production&quot;,<br>    });<br><br>    const refreshToken = cookies.get(&quot;refreshToken&quot;);<br>    if (!refreshToken) {<br>      throw new TRPCError({<br>        code: &quot;UNAUTHORIZED&quot;,<br>        message: &quot;Refresh token is required&quot;,<br>      });<br>    }<br><br>    const accessToken = await super.refreshAccessToken(refreshToken);<br>    cookies.set(&quot;accessToken&quot;, accessToken, { ...accessTokenCookieOptions });<br>    cookies.set(&quot;logged_in&quot;, &quot;true&quot;, { ...accessTokenCookieOptions });<br><br>    return { success: true };<br>  }<br>...</pre><pre>// auth.routes.ts<br><br>...<br>  refreshAccessToken: protectedProcedure.mutation(({ ctx }) =&gt;<br>    new AuthController().refreshAccessTokenHandler(ctx)<br>  ),<br>...</pre><h4>Implement logout</h4><p>Finally, let’s implement logout endpoint. For this, all we have to do is clear Redis and cookies.</p><p>Two types of logouts:</p><ol><li><strong>Single session</strong>: Clear currently active session i.e. the device the user currently using and want to logout from.</li></ol><pre>// auth.controller.ts<br><br>...<br>  async logoutHandler(ctx: AuthenticatedContext) {<br>    const { req, res, user } = ctx;<br><br>    try {<br>      const cookies = new Cookies(req, res, {<br>        secure: process.env.NODE_ENV === &quot;production&quot;,<br>      });<br>      const refreshToken = cookies.get(&quot;refreshToken&quot;);<br><br>      if (refreshToken) {<br>        await redis.del(`refresh_token:${refreshToken}`);<br>        await redis.srem(`refresh_tokens:${user.id}`, refreshToken);<br>      }<br><br>      cookies.set(&quot;accessToken&quot;, &quot;&quot;, { ...accessTokenCookieOptions });<br>      cookies.set(&quot;refreshToken&quot;, &quot;&quot;, { ...refreshTokenCookieOptions });<br>      cookies.set(&quot;logged_in&quot;, &quot;false&quot;, { ...accessTokenCookieOptions });<br><br>      return { success: true };<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>        message: &quot;Something went wrong&quot;,<br>      });<br>    }<br>  }<br>...</pre><pre>// auth.routes.ts<br><br>...<br>  logout: protectedProcedure.mutation(({ ctx }) =&gt;<br>    new AuthController().logoutHandler(ctx)<br>  ),<br>...</pre><ol><li><strong>All sessions</strong>: This is often given as security feature if user notices some suspicious activity happening in their account. (I’ve seen a lot of products that don’t give this feature and I truly hate them.)</li></ol><pre>// auth.controller.ts<br><br>...<br>  async logoutAllHandler(ctx: AuthenticatedContext) {<br>    const { req, res, user } = ctx;<br><br>    try {<br>      const refreshTokens = await redis.smembers(`refresh_tokens:${user.id}`);<br><br>      const pipeline = redis.pipeline();<br><br>      refreshTokens.forEach((refreshToken) =&gt; {<br>        pipeline.del(`refresh_token:${refreshToken}`);<br>      });<br>      pipeline.del(`refresh_tokens:${user.id}`);<br>      pipeline.del(`user:${user.id}`);<br><br>      await pipeline.exec();<br><br>      const cookies = new Cookies(req, res, {<br>        secure: process.env.NODE_ENV === &quot;production&quot;,<br>      });<br>      cookies.set(&quot;accessToken&quot;, &quot;&quot;, { ...accessTokenCookieOptions });<br>      cookies.set(&quot;refreshToken&quot;, &quot;&quot;, { ...refreshTokenCookieOptions });<br>      cookies.set(&quot;logged_in&quot;, &quot;false&quot;, { ...accessTokenCookieOptions });<br><br>      return { success: true };<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>        message: &quot;Something went wrong&quot;,<br>      });<br>    }<br>  }<br>...</pre><pre>// auth.routes.ts<br><br>...<br>  logoutAll: protectedProcedure.mutation(({ ctx }) =&gt;<br>    new AuthController().logoutAllHandler(ctx)<br>  ),<br>...</pre><h4>Set up auth middleware</h4><p>Let’s set up tRPC context and middleware and pass user data for authenticated requests, so we can use that to create protected procedures.</p><ol><li>First, open src/trpc.ts and modify createContext to check for authenticated requests.</li></ol><p>Steps:</p><ol><li>Get accessToken from request headers.</li><li>Verify access token.</li><li>Check if the user has an active session in Redis.</li><li>Check if the user still exists in the db.</li><li>Return req, and res in the tRPC context. And user object if it is an authenticated request.</li></ol><pre>// src/trpc.ts<br>import Cookies from &quot;cookies&quot;;<br><br>export const createContext = async ({<br>  req,<br>  res,<br>}: CreateExpressContextOptions) =&gt; {<br>  try {<br>    const cookies = new Cookies(req, res);<br>    const accessToken = cookies.get(&quot;accessToken&quot;);<br>    if (!accessToken) {<br>      return { req, res };<br>    }<br><br>    const userId = await new AuthService().verifyAccessToken(accessToken);<br>    if (!userId) {<br>      return { req, res };<br>    }<br><br>    const session = await redis.get(`user:${userId}`);<br>    if (!session) {<br>      return { req, res };<br>    }<br><br>    const user = (<br>      await db<br>        .select()<br>        .from(users)<br>        .where(eq(users.id, parseInt(userId)))<br>        .limit(1)<br>    )[0];<br>    if (!user) {<br>      return { req, res };<br>    }<br><br>    return {<br>      req,<br>      res,<br>      user,<br>    };<br>  } catch (error) {<br>    console.log(error);<br><br>    throw new TRPCError({<br>      code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>      message: &quot;Something went wrong&quot;,<br>    });<br>  }<br>};</pre><p>We can use this to identify which requests are authenticated.</p><ol><li>Now let’s create a reusable tRPC procedure called protectedProcedure to protect some endpoints and a tRPC middleware called isAuthenticted.</li></ol><pre>// src/trpc.ts<br><br>// Create another context type for protected routes, so ctx.user won&#39;t be null in authed requests<br>export type AuthenticatedContext = Context &amp; {<br>  user: NonNullable&lt;Context[&quot;user&quot;]&gt;;<br>};<br><br>// Middleware to check if user is authenticated<br>const isAuthenticated = t.middleware(({ ctx, next }) =&gt; {<br>  if (!ctx.user) {<br>    throw new TRPCError({<br>      code: &quot;UNAUTHORIZED&quot;,<br>      message: &quot;You must be logged in to access this resource&quot;,<br>    });<br>  }<br><br>  return next({<br>    ctx: {<br>      ...ctx,<br>      user: ctx.user,<br>    },<br>  });<br>});<br><br>// Using the middleware, create a protected procedure<br>export const protectedProcedure = publicProcedure.use(isAuthenticated);</pre><blockquote>In a later article, we will create proProtectedProcedure for paid features when integrating a payment provider 👀. Follow for updates 🤫.</blockquote><p>Now use this procedure in all transaction routes.</p><pre>// src/modules/transaction/transaction.routes.ts<br><br>const transactionRouter = router({<br>  create: protectedProcedure // &lt;--- here<br>    .input(insertUserSchema)<br>    .mutation(({ input, ctx }) =&gt; <br>      new TransactionController().createTransactionHandler(input, ctx) // pass context <br>    ),<br><br>  getAll: protectedProcedure.query(({ ctx }) =&gt; // &lt;--- here<br>    new TransactionController().getTransactionsHandler(ctx) // pass context <br>  ),<br>});</pre><p>Then, open transaction.controller.ts and modify it like this to use the user id from ctx.user:</p><pre>...<br>  async createTransactionHandler(<br>    data: Omit&lt;typeof transactions.$inferInsert, &quot;userId&quot;&gt;, // &lt;-- Omit userId as we get it from ctx, not input<br>    ctx: AuthenticatedContext // &lt;-- add ctx param<br>  ) {<br>    return await super.createTransaction({<br>      ...data,<br>      userId: ctx.user.id,<br>    });<br>  }<br><br>  async getTransactionsHandler(ctx: AuthenticatedContext) { // &lt;-- same here<br>    return await super.getTransactions(ctx.user.id);<br>  }<br>...</pre><h4>Test Everything</h4><p>Let’s test our API using Postman to verify everything is working as expected. Testing tRPC API in Postman is a little different than traditional REST/Graphql APIs.</p><p>Limitations:</p><ul><li>Cannot use superjson. So, before testing let&#39;s comment out superjson transformer in trpc.ts.</li></ul><pre>const t = initTRPC.context&lt;Context&gt;().create({<br>  // transformer: SuperJSON,<br>});</pre><ul><li>You cannot test queries. For this, just change query to mutation. After testing, make sure to revert changes.</li></ul><ol><li>POST /auth.register</li></ol><figure><img alt="register test" src="https://cdn-images-1.medium.com/max/1024/0*wgf-6iIuax12vCyS.png" /></figure><ol><li>POST /auth.login</li></ol><figure><img alt="login test" src="https://cdn-images-1.medium.com/max/1024/0*pLC0JRogS8nyX8kv.png" /></figure><p>If you have <a href="https://redis.io/insight">Redis Insight</a> downloaded, you can easily see your keys.</p><figure><img alt="redis insight" src="https://cdn-images-1.medium.com/max/1024/0*4fWds89uPZpDSzLz.png" /></figure><ol><li>POST /auth.refreshAccessToken</li></ol><figure><img alt="refresh test" src="https://cdn-images-1.medium.com/max/1024/0*FqDq6ZG4915oGwwG.png" /></figure><ol><li>POST /auth.logout</li></ol><p>If you observe the cookies and headers tab, you can see tokens are empty and if you check Redis insight, the refresh token will be deleted. Same with /auth.logoutAll but this time, all refresh tokens belonging to the user including the user session will be deleted.</p><ol><li>Also, after logging in, try to test transaction routes.</li></ol><p>That’s it!</p><p>In the next article, I will share how to secure the Next.js front end.</p><p>Follow for updates 🚀.</p><p><a href="https://rksh.link/links">Socials</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b6c1b2710b0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/implementing-cookie-based-jwt-authentication-in-a-trpc-backend-6b6c1b2710b0">Implementing Cookie-Based JWT Authentication in a tRPC Backend</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My 2025 Tech Stack: Tools & Tech I’m Using This Year]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/my-2025-tech-stack-tools-tech-im-using-this-year-ca06af68b8da?source=rss-e09c62468ad2------2"><img src="https://cdn-images-1.medium.com/max/1600/1*jjJ7IjgexU1YZbrBKS-r1w.png" width="1600"></a></p><p class="medium-feed-link"><a href="https://medium.com/codex/my-2025-tech-stack-tools-tech-im-using-this-year-ca06af68b8da?source=rss-e09c62468ad2------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/my-2025-tech-stack-tools-tech-im-using-this-year-ca06af68b8da?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/ca06af68b8da</guid>
            <category><![CDATA[tools]]></category>
            <category><![CDATA[tech-stack]]></category>
            <category><![CDATA[devtools]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Wed, 01 Jan 2025 17:25:39 GMT</pubDate>
            <atom:updated>2025-01-02T10:09:18.625Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Setting Up Drizzle & Postgres with tRPC and Next.js App]]></title>
            <link>https://medium.com/codex/setting-up-drizzle-postgres-with-trpc-and-next-js-app-15fd8af68485?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/15fd8af68485</guid>
            <category><![CDATA[trpc]]></category>
            <category><![CDATA[drizzle]]></category>
            <category><![CDATA[postgresql]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Wed, 25 Dec 2024 14:14:44 GMT</pubDate>
            <atom:updated>2025-01-13T09:15:09.404Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F-AiLcLAW_vnQaIM0HLXvQ.png" /><figcaption>Setting Up Drizzle &amp; Postgres with tRPC and Next.js App</figcaption></figure><p><em>Published from </em><a href="https://www.publishstudio.one"><em>Publish Studio</em></a></p><p>In this tutorial, let’s learn how to connect a Postgres database to a tRPC express backend using Drizzle ORM. I have also created a simple frontend for our finance tracker application. You can copy frontend code from the repo <a href="https://github.com/itsrakeshhq/finance-tracker/tree/main/frontend">here</a>.</p><figure><img alt="finance tracker" src="https://cdn-images-1.medium.com/max/1024/0*PdVHJKLCS6oKupzI.png" /></figure><p>This is part 2, read part 1 here: <a href="https://itsrakesh.com/blog/lets-build-a-full-stack-app-with-trpc-and-nextjs-14">Let’s Build a Full-Stack App with tRPC and Next.js 14</a></p><h4>Backend</h4><p>If you don’t have Postgres installed locally, please do or you can also use a hosted database.</p><p>Once you have Postgres ready, add DATABASE_URL to your .env:</p><pre>DATABASE_URL=postgres://postgres:password@localhost:5432/myDB</pre><h4>Setting up db with drizzle</h4><p>To set up drizzle, start off by installing these packages:</p><pre>yarn add drizzle-orm pg dotenv<br>yarn add -D drizzle-kit tsx @types/pg</pre><p>Now, all you have to do is connect drizzle to the DB. To do that, create src/utils/db.ts file and configure drizzle:</p><pre>import { drizzle } from &quot;drizzle-orm/node-postgres&quot;;<br>import pg from &quot;pg&quot;;<br>const { Pool } = pg;<br><br>export const pool = new Pool({<br>  connectionString: process.env.DATABASE_URL,<br>  ssl: process.env.NODE_ENV === &quot;production&quot;,<br>});<br><br>export const db = drizzle(pool);</pre><p>That’s it! Our db setup is ready. We can now create tables and interact with our db using drizzle ORM.</p><h4>Create the first module</h4><p>Regarding the project structure, there are mainly two types:</p><ol><li>Modules: Divide features into different modules and keep all related files together. Popular frameworks like NestJs and Angular use this structure.</li></ol><pre>.<br>└── feature/<br>    ├── feature.controller.ts<br>    ├── feature.routes.ts<br>    ├── feature.schema.ts<br>    └── feature.service.ts</pre><ol><li>Separate folders based on the purpose:</li></ol><pre>.<br>├── controllers/<br>│   ├── feature1.controller.ts<br>│   └── feature2.controller.ts<br>├── services/<br>│   ├── feature1.service.ts<br>│   └── feature2.service.ts<br>└── models/<br>    ├── feature1.model.ts<br>    └── feature2.model.ts</pre><p>I personally prefer modules because it just makes sense (<em>plz stop using 2nd one</em>).</p><p>Now, let’s create our first module called transaction. This is our core feature. Start by creating src/modules/transaction/transaction.schema.ts file. This is where we define transaction schema using drizzle.</p><p>The great thing about using drizzle to write schemas is it lets us use typescript. So you don’t have to learn a new syntax and ensure type safety for your schemas.</p><p>To record a transaction (txn), the most basic things we need are:</p><ul><li>txn amount</li><li>txn type — credit or debit</li><li>description — a simple note to refer to later</li><li>tag — a category like shopping/travel/food, and so on.</li></ul><p>First, let’s create enums for txn type and tag:</p><pre>import {<br>  pgEnum,<br>} from &quot;drizzle-orm/pg-core&quot;;<br><br>export const txnTypeEnum = pgEnum(&quot;txnType&quot;, [&quot;Incoming&quot;, &quot;Outgoing&quot;]);<br>export const tagEnum = pgEnum(&quot;tag&quot;, [<br>  &quot;Food&quot;,<br>  &quot;Travel&quot;,<br>  &quot;Shopping&quot;,<br>  &quot;Investment&quot;,<br>  &quot;Salary&quot;,<br>  &quot;Bill&quot;,<br>  &quot;Others&quot;,<br>]);</pre><p>Then, let’s create the schema:</p><pre>import {<br>  integer,<br>  pgTable,<br>  serial,<br>  text,<br>  timestamp,<br>} from &quot;drizzle-orm/pg-core&quot;;<br><br>export const transactions = pgTable(&quot;transactions&quot;, {<br>  id: serial(&quot;id&quot;).primaryKey(),<br>  amount: integer(&quot;amount&quot;).notNull(),<br>  txnType: txnTypeEnum(&quot;txn_type&quot;).notNull(),<br>  summary: text(&quot;summary&quot;),<br>  tag: tagEnum(&quot;tag&quot;).default(&quot;Others&quot;),<br>  createdAt: timestamp(&quot;created_at&quot;).defaultNow(),<br>  updatedAt: timestamp(&quot;updated_at&quot;)<br>    .defaultNow()<br>    .$onUpdate(() =&gt; new Date()),<br>});</pre><p>As you can see, we simply wrote typescript code and created a table!</p><h4>Run migrations</h4><p>One final step before we can start interacting with our db is to apply changes to our database so that all the tables will be created. To do that we have to run migrations. Drizzle has this amazing tool called drizzle-kit which handles migrations for us, so all we have to do is run a command.</p><p>Before doing that we have to create a file called drizzle.config.ts in the project root, which includes all the information about the database and schemas.</p><pre>import &quot;dotenv/config&quot;;<br>import { defineConfig } from &quot;drizzle-kit&quot;;<br><br>export default defineConfig({<br>  schema: &quot;./src/**/*.schema.ts&quot;,<br>  out: &quot;./drizzle&quot;,<br>  dialect: &quot;postgresql&quot;,<br>  dbCredentials: {<br>    url: process.env.DATABASE_URL!,<br>    ssl: process.env.NODE_ENV === &quot;production&quot;,<br>  },<br>});</pre><p>With that ready, run the below command:</p><pre>yarn dlx drizzle-kit push</pre><p>That’s it! Now we can start interacting with db and write our business logic.</p><h4>Business logic</h4><p>Let’s add logic to add new transactions.</p><p>If you already don’t know:</p><ul><li>Service — where we interact with DB and write most of the business logic</li><li>Controller — handle request/response</li></ul><p>Create transaction/transaction.service.ts and write logic to add new transactions to db:</p><pre>import { TRPCError } from &quot;@trpc/server&quot;;<br>import { db } from &quot;../../utils/db&quot;;<br>import { transactions } from &quot;./transaction.schema&quot;;<br><br>export default class TransactionService {<br>  async createTransaction(data: typeof transactions.$inferInsert) {<br>    try {<br>      return await db.insert(transactions).values(data).returning();<br>    } catch (error) {<br>      console.log(error);<br><br>      throw new TRPCError({<br>        code: &quot;INTERNAL_SERVER_ERROR&quot;,<br>        message: &quot;Failed to create transaction&quot;,<br>      });<br>    }<br>  }<br>}</pre><p>Another benefit of using drizzle ORM is it provides type definitions for different CRUD methods like $inferInsert, $inferSelect so there is no need to define the types again. Here, by using typeof transactions.$inferInsert we don&#39;t have to provide values for fields like primary key, and fields with default values like createdAt, and updatedAt, so typescript won&#39;t throw an error.</p><p>Drizzle also has extensions like <a href="https://orm.drizzle.team/docs/zod">drizzle-zod</a> which can be used to generate zod schemas. Another headache was prevented by drizzle 🫡. So open transaction.schema.ts and create zod schema for insert operation:</p><pre>import { createInsertSchema } from &quot;drizzle-zod&quot;;<br><br>export const insertUserSchema = createInsertSchema(transactions).omit({<br>  id: true,<br>  createdAt: true,<br>  updatedAt: true,<br>});</pre><p>Let’s use this in the controller, create transaction/transaction.controller.ts:</p><pre>export default class TransactionController extends TransactionService {<br>  async createTransactionHandler(data: typeof transactions.$inferInsert) {<br>    return await super.createTransaction(data);<br>  }<br>}</pre><p>Now, all that remains is to expose this controller through an endpoint. For that, create transaction/transaction.routes.ts. Since we are using tRPC, to create an endpoint, we have to define a procedure:</p><pre>import { publicProcedure, router } from &quot;../../trpc&quot;;<br>import TransactionController from &quot;./transaction.controller&quot;;<br>import { insertUserSchema } from &quot;./transaction.schema&quot;;<br><br>const transactionRouter = router({<br>  create: publicProcedure<br>    .input(insertUserSchema)<br>    .mutation(({ input }) =&gt;<br>      new TransactionController().createTransactionHandler(input)<br>    ),<br>});<br><br>export default transactionRouter;</pre><p>If you remember from part 1, we created a reusable router that can be used to group procedures and publicProcedure which creates an endpoint.</p><p>Finally, open src/routes.ts and use the above transactionRouter:</p><pre>import transactionRouter from &quot;./modules/transaction/transaction.routes&quot;;<br>import { router } from &quot;./trpc&quot;;<br><br>const appRouter = router({<br>  transaction: transactionRouter,<br>});<br><br>export default appRouter;</pre><p>That’s it! The backend is ready. This is the final backend structure:</p><pre>.<br>├── README.md<br>├── drizzle<br>│   ├── 0000_true_junta.sql<br>│   └── meta<br>│       ├── 0000_snapshot.json<br>│       └── _journal.json<br>├── drizzle.config.ts<br>├── package.json<br>├── src/<br>│   ├── index.ts<br>│   ├── modules/<br>│   │   └── transaction/<br>│   │       ├── transaction.controller.ts<br>│   │       ├── transaction.routes.ts<br>│   │       ├── transaction.schema.ts<br>│   │       └── transaction.service.ts<br>│   ├── routes.ts<br>│   ├── trpc.ts<br>│   └── utils/<br>│       ├── db.ts<br>│       └── migrate.ts<br>├── tsconfig.json<br>└── yarn.lock</pre><h4>Challenge for you</h4><p>Before proceeding to frontend integration, as a challenge, create an endpoint for getting all transactions.</p><h4>Frontend</h4><p>It’s time to integrate the created endpoints in our frontend. Since this is not a frontend tutorial, I’ll let you just copy the code from the repo.</p><p>All I’ve changed is:</p><ul><li>Set up <a href="https://ui.shadcn.com/">shadcn/ui</a></li><li>Change src/components/modules/dashboard/index.tsx</li></ul><p>Also, as you observe, I’m using a modules-like structure here too. If you also like this structure, you can learn more from my previous projects <a href="https://github.com/RakeshPotnuru/publish-studio">Publish Studio</a> and <a href="https://github.com/RakeshPotnuru/myonepost">My One Post</a></p><p>In part 1, we queried data using built-in tRPC react-query.</p><pre>...<br>  const { data } = trpc.test.useQuery();<br><br>  return (<br>    &lt;main className=&quot;flex min-h-screen flex-col items-center justify-between p-24&quot;&gt;<br>      {data}<br>    &lt;/main&gt;<br>  );<br>...</pre><p>So, if you already know react-query, there’s isn’t much to learn except with tRPC we don’t have to create queryFn or mutationFn because we directly call backend methods.</p><p>This is how mutations are used:</p><pre>...<br>  const { mutateAsync: createTxn, isLoading: isCreating } =<br>    trpc.transaction.create.useMutation({<br>      onSuccess: async () =&gt; {<br>        form.reset();<br>        await utils.transaction.getAll.invalidate();<br>      },<br>    });<br>    <br>  const addTransaction = async (data: z.infer&lt;typeof formSchema&gt;) =&gt; {<br>    try {<br>      await createTxn(data);<br>    } catch (error) {<br>      console.error(error);<br>    }<br>  };<br>...</pre><h4>See In Action</h4><figure><img alt="completed tutorial" src="https://cdn-images-1.medium.com/max/800/0*dtNDZTVovWF3aCpD.gif" /></figure><p>I hope you like this tutorial. Feel free to extend the functionality. In the next article, I’ll share how to add authentication.</p><p>Project source code can be found <a href="https://github.com/itsrakeshhq/finance-tracker">here</a>.</p><p>Follow me for more 🚀. <a href="https://rksh.link/links">Socials</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15fd8af68485" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/setting-up-drizzle-postgres-with-trpc-and-next-js-app-15fd8af68485">Setting Up Drizzle &amp; Postgres with tRPC and Next.js App</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Let’s Build a Full-Stack App with tRPC and Next.js App Router]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/design-bootcamp/lets-build-a-full-stack-app-with-trpc-and-next-js-14-a679acd4ab2d?source=rss-e09c62468ad2------2"><img src="https://cdn-images-1.medium.com/max/1600/1*3KL4VjZXxR_AYTPPrYG1DA.png" width="1600"></a></p><p class="medium-feed-snippet">Ready to build blazing-fast, type-safe full-stack applications? Dive into the power of tRPC and Next.js 14!</p><p class="medium-feed-link"><a href="https://medium.com/design-bootcamp/lets-build-a-full-stack-app-with-trpc-and-next-js-14-a679acd4ab2d?source=rss-e09c62468ad2------2">Continue reading on Bootcamp »</a></p></div>]]></description>
            <link>https://medium.com/design-bootcamp/lets-build-a-full-stack-app-with-trpc-and-next-js-14-a679acd4ab2d?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/a679acd4ab2d</guid>
            <category><![CDATA[nextjs]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[full-stack]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Fri, 19 Jul 2024 09:24:19 GMT</pubDate>
            <atom:updated>2024-07-22T11:11:44.552Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[✨ Introducing Publish Studio]]></title>
            <link>https://medium.com/@itsrakesh/introducing-publish-studio-56681e27e767?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/56681e27e767</guid>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Tue, 05 Mar 2024 05:30:07 GMT</pubDate>
            <atom:updated>2024-03-20T13:35:36.744Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AYR5HPF2nl-HrSzk1Y6ZHA.png" /></figure><p><em>Published from </em><a href="https://publishstudio.one"><em>Publish Studio</em></a></p><p><strong>UPDATE:</strong> Publish Studio is now generally available. <a href="https://app.publishstudio.one/register">Sign Up</a> for free and start writing.</p><p>It’s been a really long time since I last published an article here. I’ve been inactive due to some personal career reasons. But I’m back now with so many exciting things I learned last year and can’t wait to share them with you.</p><h4>Quick Intro</h4><p>First of all, say “Hi” to <a href="https://publishstudio.one/">Publish Studio</a>, a platform I’ve building for the past few months. If you are a content writer, then you should definitely check it out. And if you are someone who has an audience on multiple blogging platforms and need an easy way to manage your content across platforms, then you should 100% give it a try.</p><p>Currently, Publish Studio is in beta, so please join the waitlist. I would appreciate your feedback on any issues before making it generally available.</p><p>Join waitlist: <a href="https://publishstudio.one">https://publishstudio.one</a></p><figure><img alt="1.png" src="https://cdn-images-1.medium.com/max/1024/0*agchEOo--Rz988wB.png" /></figure><h4>Main Features</h4><h4>1. Rich Text Editing</h4><p>Elevate your content with rich text editing. It provides you with robust formatting tools to refine your writing and make it visually appealing.</p><figure><img alt="2.png" src="https://cdn-images-1.medium.com/max/1024/0*UUbxKHUGfF-DSD-8.png" /></figure><h4>2. Dictation</h4><p>Simplify your content creation process with the dictation feature. Speak naturally, and let the platform transcribe your words accurately into text. It’s a convenient tool that saves time and effort, allowing you to focus on expressing your ideas effectively.</p><h4>3. Tone Analysis</h4><p>Gain insights into the emotional tone of your content with the tone analysis feature. It helps you understand how your writing resonates with your audience, enabling you to fine-tune your messaging accordingly. Whether you aim for a casual or formal tone, the analysis tool assists you in crafting content that connects authentically.</p><figure><img alt="6.png" src="https://cdn-images-1.medium.com/max/1024/0*_1Pywt_0pORXCRQ3.png" /></figure><h4>4. Import Content</h4><p>If you got your stuff scattered everywhere, you can bring them all together into the platform with the import feature, making it easy to consolidate your content library in one place.</p><figure><img alt="5.png" src="https://cdn-images-1.medium.com/max/1024/0*P7ROTbeixbL2u72x.png" /></figure><h4>5. Integrated Media Tools</h4><p>Integrated media tools! With access to gems like Pexels, Unsplash, Imagekit, and Cloudinary right on the platform.</p><figure><img alt="4.png" src="https://cdn-images-1.medium.com/max/1024/0*_CkX-AbFzgNSYhzb.png" /></figure><h4>6. Export Content</h4><p>A small but helpful feature. Export your content in different file formats.</p><h4>7. Publish to Multiple Platforms</h4><p><em>I want to clear up any confusion that might come in the future. Publishing to multiple platforms is one of the </em><strong><em>core features</em></strong><em> of Publish Studio but it’s </em><strong><em>not the only goal</em></strong><em>.</em></p><p>With Publish Studio, publishing to multiple platforms is a breeze! Spread your content far and wide with just a few clicks.</p><figure><img alt="3.png" src="https://cdn-images-1.medium.com/max/1024/0*I3Aij9EBkYtxE_DX.png" /></figure><h4>8. Schedule Posts</h4><p>You don’t have to publish right away. With the schedule feature, you’re the master of your content calendar. Plan ahead, set the dates, and let Publish Studio do the rest.</p><h4>9. Writing Stats</h4><p>Curious about your writing habits? Dive into your writing stats and discover your creative superpowers!</p><p>…and a lot more already planned but need a little time and your big support ❤️.</p><p>This is just the start with more features coming in the future.</p><p>Follow me for more! Onwards and upwards 🚀.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=56681e27e767" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploying a MERN App to AWS Elastic Beanstalk with CI/CD]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/deploying-a-mern-app-to-aws-elastic-beanstalk-with-ci-cd-843f414645ec?source=rss-e09c62468ad2------2"><img src="https://cdn-images-1.medium.com/max/1600/1*5CyfyW0BqEzqC-TFr0pTgQ.png" width="1600"></a></p><p class="medium-feed-link"><a href="https://medium.com/codex/deploying-a-mern-app-to-aws-elastic-beanstalk-with-ci-cd-843f414645ec?source=rss-e09c62468ad2------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/deploying-a-mern-app-to-aws-elastic-beanstalk-with-ci-cd-843f414645ec?source=rss-e09c62468ad2------2</link>
            <guid isPermaLink="false">https://medium.com/p/843f414645ec</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[beginners-guide]]></category>
            <category><![CDATA[elastic-beanstalk]]></category>
            <dc:creator><![CDATA[Rakesh Potnuru]]></dc:creator>
            <pubDate>Fri, 24 Feb 2023 20:37:04 GMT</pubDate>
            <atom:updated>2024-01-21T14:32:35.753Z</atom:updated>
        </item>
    </channel>
</rss>