<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rekha Suthar</title>
    <description>The latest articles on DEV Community by Rekha Suthar (@rekha0suthar).</description>
    <link>https://dev.to/rekha0suthar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3922779%2F764ceaf8-8f48-4e3c-9fb9-79df9fb36f2f.png</url>
      <title>DEV Community: Rekha Suthar</title>
      <link>https://dev.to/rekha0suthar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rekha0suthar"/>
    <language>en</language>
    <item>
      <title>Building AI Resume Tailor — v0 build notes</title>
      <dc:creator>Rekha Suthar</dc:creator>
      <pubDate>Sun, 10 May 2026 09:58:15 +0000</pubDate>
      <link>https://dev.to/rekha0suthar/building-ai-resume-tailor-v0-build-notes-58f9</link>
      <guid>https://dev.to/rekha0suthar/building-ai-resume-tailor-v0-build-notes-58f9</guid>
      <description>&lt;p&gt;&lt;em&gt;A short build log on shipping the first AI feature for my portfolio: paste a resume + a job description, get back tailored bullet rewrites, ATS keyword gaps, and likely interview questions. End-to-end on free tools.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rekha0suthar/ai-resume-tailor" rel="noopener noreferrer"&gt;AI Resume Tailor&lt;/a&gt;(&lt;a href="https://ai-resume-tailor-ruby.vercel.app/" rel="noopener noreferrer"&gt;Live Link&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Two textareas. One button. Behind the button: a Vercel serverless function that calls Groq's Llama 3.3 70B in JSON mode and returns three things — a list of tailored bullet rewrites, a missing-keyword gap analysis, and 5 predicted interview questions with prep tips. Plus a 0–100 match score.&lt;/p&gt;

&lt;p&gt;It's the same prompt-engineering, structured-output, "talk to an LLM and render the response cleanly" pattern that AI app companies are hiring for every day. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack — and why each piece
&lt;/h2&gt;

&lt;p&gt;I picked everything by one rule: &lt;strong&gt;it must be free, no credit card, with enough quota to actually use the app.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite + React 18 + Tailwind CSS&lt;/strong&gt; for the frontend. Vite over CRA because it's faster, and CRA is unmaintained. Tailwind because shipping a tiny single-page app shouldn't need 600 lines of CSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Hobby tier&lt;/strong&gt; for hosting. The killer feature for this project: serverless functions live in the same repo as the frontend. Drop a file in &lt;code&gt;/api/tailor.js&lt;/code&gt; and it becomes a routable endpoint. No second hosting account, no CORS dance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq Cloud&lt;/strong&gt; for inference. Free API key, no credit card, very fast inference (Llama 3.3 70B at ~300 tokens/sec). Their JSON mode forces the model to return parseable JSON, which removes a whole category of "the model added markdown around its answer" bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The single most important architecture decision: &lt;strong&gt;the API key never leaves the server.&lt;/strong&gt; Browsers can read every script you ship, so an API call that includes the Groq key client-side is a key any visitor can extract. The serverless function is the firewall — the browser POSTs &lt;code&gt;/api/tailor&lt;/code&gt;, the function reads the key from &lt;code&gt;process.env.GROQ_API_KEY&lt;/code&gt;, talks to Groq, and pipes back only the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt that does the work
&lt;/h2&gt;

&lt;p&gt;Most of the value is in the system prompt. Three rules I leaned on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Force structured output.&lt;/strong&gt; The system prompt declares the exact JSON shape and Groq's &lt;code&gt;response_format: { type: 'json_object' }&lt;/code&gt; is the enforcer. Without that, Llama would occasionally wrap the answer in markdown — &lt;code&gt;"Here's your tailored resume: { … }"&lt;/code&gt; — and &lt;code&gt;JSON.parse&lt;/code&gt; would explode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Be honest, don't invent.&lt;/strong&gt; A career coach who fabricates skills is worse than no coach. The system prompt explicitly says: &lt;em&gt;"Be honest. If the resume is weak for the role, say so via the score and the missing-keywords section — do NOT invent skills the candidate doesn't have."&lt;/em&gt; Without that line, the model is too eager to please and quietly upgrades "I built a CRUD app" to "Led architecture for distributed systems."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ground every output in something concrete.&lt;/strong&gt; Each tailored bullet ships with both the original line and the rewrite, so the user can see the diff. Each missing keyword ships with an actionable suggestion ("Add a 1-line bullet about your school project on X" instead of "Learn TypeScript"). Each interview question ships with what-they-want and a prep-tip. Specificity beats vagueness, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost at this scale
&lt;/h2&gt;

&lt;p&gt;Free, currently. Groq's Llama 3.3 70B free tier gives you ~14,400 requests/day with a per-minute rate limit. Each tailor request uses ~5,000 tokens in + ~1,500 out, well under any single limit. Vercel Hobby gives 100k function invocations and 100 GB-hours of compute per month. For a portfolio piece that maybe handles 50 requests/day from recruiters poking at it, both tiers are wildly oversized.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still v0
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No streaming yet.&lt;/strong&gt; The user clicks Tailor, sees a spinner for ~5 seconds, then the full result lands. v1 should stream — partial output as it generates, even if just the bullets section first. SSE on the function side, &lt;code&gt;ReadableStream&lt;/code&gt; reader on the client. That's the next iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No PDF upload.&lt;/strong&gt; Right now it's paste-only. Most people have their resume as a PDF, not as plain text. Adding &lt;code&gt;pdfjs-dist&lt;/code&gt; to extract text client-side is maybe an hour's work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No history.&lt;/strong&gt; Run it twice on different JDs and the previous result is gone. &lt;code&gt;localStorage&lt;/code&gt; is enough for a v1 — no backend needed. Shareable result links would require an actual database; not worth it yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No streaming of the &lt;em&gt;thinking&lt;/em&gt;.&lt;/strong&gt; Llama doesn't expose chain-of-thought, so we don't get to show "thinking..." with intermediate steps. A streaming UI does the same job perceptually, which is why v1 is streaming-first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trickiest bug so far
&lt;/h2&gt;

&lt;p&gt;Llama 3.3, even in JSON mode, occasionally returns valid JSON with the &lt;em&gt;wrong shape&lt;/em&gt; — say, &lt;code&gt;tailored_bullets&lt;/code&gt; as a string of bullets joined with newlines instead of an array of objects. JSON mode protects you from "is this parseable JSON" but not "is this the JSON I asked for."&lt;/p&gt;

&lt;p&gt;Two defenses, in order of cost:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Be explicit in the system prompt.&lt;/strong&gt; I literally pasted the JSON skeleton with empty strings inside (&lt;code&gt;{ "tailored_bullets": [{"original": "", "rewrite": "", "why": ""}], … }&lt;/code&gt;) and told the model "return ONLY valid JSON matching this exact shape." That alone fixed ~95% of drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate on the client.&lt;/strong&gt; The React app accesses &lt;code&gt;result?.tailored_bullets&lt;/code&gt; with optional chaining and &lt;code&gt;[]&lt;/code&gt; fallback for every list. If the model returns junk, the UI just shows fewer panels — no white-screen crash. Cheap, correct, ships.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A future v2 would replace the prompt-engineering defence with a Zod schema or a JSON Schema constrained-decoding layer. For v0, prompt + optional chaining is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rekha0suthar/ai-resume-tailor" rel="noopener noreferrer"&gt;github.com/rekha0suthar/ai-resume-tailor&lt;/a&gt; · MIT licensed. Fork it, deploy your own. The whole thing is ~600 lines including the README.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next up: streaming the response (v1) and PDF upload (v2). I'll post the streaming UX write-up after v1 ships — designing "AI is thinking" loading states is its own discipline.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>llm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Role-based access in a MERN e-commerce app</title>
      <dc:creator>Rekha Suthar</dc:creator>
      <pubDate>Sun, 10 May 2026 04:08:27 +0000</pubDate>
      <link>https://dev.to/rekha0suthar/role-based-access-in-a-mern-e-commerce-app-p0e</link>
      <guid>https://dev.to/rekha0suthar/role-based-access-in-a-mern-e-commerce-app-p0e</guid>
      <description>&lt;p&gt;&lt;em&gt;A short walkthrough of how I structured permissions for customers, admins, and store managers in &lt;a href="https://grocery-store-ruddy-eight.vercel.app/" rel="noopener noreferrer"&gt;Grocery Store&lt;/a&gt; — what worked, what I'd change, and the one bug that taught me to never trust the client.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;When I started building Grocery Store, I had three user types in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt; — browse the catalog, add to cart, check out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store managers&lt;/strong&gt; — add or edit products, manage inventory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admins&lt;/strong&gt; — everything a store manager can do, plus user management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds like a clean three-role hierarchy on paper. In practice, getting it right takes more than dropping a &lt;code&gt;role&lt;/code&gt; field on the user model. Here's how I structured it, and the rough edges I ran into along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The user shape
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; model carries a single &lt;code&gt;role&lt;/code&gt; field with one of three values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// models/User.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single source of truth. Customers self-sign up; managers and admins are seeded or promoted by an existing admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places permissions are enforced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. JWT payload.&lt;/strong&gt; When a user logs in, the role goes into the signed JWT, so every authenticated request carries it without an extra DB lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;7d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Express middleware.&lt;/strong&gt; Two layers — &lt;code&gt;requireAuth&lt;/code&gt; checks the token is valid, then &lt;code&gt;requireRole&lt;/code&gt; checks it carries the right role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware/auth.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not authorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Routes then read like a permission spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;createProduct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="nx"&gt;deleteUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. React UI.&lt;/strong&gt; A small &lt;code&gt;&amp;lt;RequireRole roles={['admin']}&amp;gt;&lt;/code&gt; wrapper hides admin-only links from the navbar and gates whole admin pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RequireRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;the UI guard is for UX, not security.&lt;/strong&gt; If a customer crafts a request to &lt;code&gt;DELETE /users/123&lt;/code&gt;, the React wrapper isn't between them and the database. The middleware is. Always assume the client is hostile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that taught me that
&lt;/h2&gt;

&lt;p&gt;Early on I had role-based UI but not role-based middleware on a couple of admin endpoints. I assumed: &lt;em&gt;"if the button isn't rendered, the customer can't reach the endpoint."&lt;/em&gt; True for honest customers. Anyone with browser dev tools and 30 seconds of curiosity can copy the request from the admin's network tab and replay it themselves.&lt;/p&gt;

&lt;p&gt;Caught it in code review with a friend. The fix was a one-line &lt;code&gt;requireRole('admin')&lt;/code&gt; add. The lesson — &lt;em&gt;every&lt;/em&gt; mutation route needs explicit role enforcement, full stop — was worth more than the bug took to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change next time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use a permission flag, not a role string.&lt;/strong&gt; &lt;code&gt;role: 'admin'&lt;/code&gt; works for three roles. The moment the product needs "managers who can read user emails but not delete them," the role field collapses. A &lt;code&gt;permissions: ['products.write', 'users.read']&lt;/code&gt; array — or a small ACL table — scales better. I'd start there if I rebuilt today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centralize the policy.&lt;/strong&gt; My current code spreads &lt;code&gt;requireRole('admin')&lt;/code&gt; calls across routes. Better: one &lt;code&gt;policy.js&lt;/code&gt; file mapping every route to required permissions, applied via a single middleware. One place to audit, one place to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit log.&lt;/strong&gt; Every action a manager or admin takes should land in an audit collection. I didn't add this and I'd regret it the moment a real product manager said "wait, who deleted that?".&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Role-based access is one of those things that &lt;em&gt;looks&lt;/em&gt; trivial — add a field, check it, done — and stays trivial as long as you remember the cardinal rule: &lt;strong&gt;the server enforces, the client suggests.&lt;/strong&gt; Build both layers, but never let the UI carry the security weight.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/rekha0suthar/grocery-store" rel="noopener noreferrer"&gt;github.com/rekha0suthar/grocery-store&lt;/a&gt; · Live: &lt;a href="https://grocery-store-ruddy-eight.vercel.app/" rel="noopener noreferrer"&gt;grocery-store-ruddy-eight.vercel.app&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next up: build notes from the AI Resume Tailor — prompt design for structured output and streaming UX in React.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
