<?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: Angel Nikolov</title>
    <description>The latest articles on DEV Community by Angel Nikolov (@angelnikolov).</description>
    <link>https://dev.to/angelnikolov</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F195588%2Faa57c4a2-3e55-4773-878e-6109579178da.png</url>
      <title>DEV Community: Angel Nikolov</title>
      <link>https://dev.to/angelnikolov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/angelnikolov"/>
    <language>en</language>
    <item>
      <title>How I stopped misclassifying jobs with Jev!</title>
      <dc:creator>Angel Nikolov</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:31:26 +0000</pubDate>
      <link>https://dev.to/angelnikolov/how-i-stopped-misclassifying-jobs-with-jev-2k0m</link>
      <guid>https://dev.to/angelnikolov/how-i-stopped-misclassifying-jobs-with-jev-2k0m</guid>
      <description>&lt;p&gt;&lt;em&gt;Why Java is not JavaScript, remote is not hybrid, and how one fan out call fixed our filters&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Regex Was Miscategorizing My Job Board. I Added a Judgment Layer With&amp;nbsp;JEV.
&lt;/h2&gt;

&lt;p&gt;We run &lt;a href="//remotefrontendjobs.com]"&gt;remotefrontendjobs.com&lt;/a&gt;, &lt;a href="//remotebackendjobs.com"&gt;remotebackendjobs.com&lt;/a&gt;, and &lt;a href="//remotejavajobs.com"&gt;remotejavajobs.com&lt;/a&gt;. Thousands of remote jobs from 20 plus sources. This is how TypeSafe AI System One Models and the Jev model fixed our AI job classification.&lt;/p&gt;

&lt;p&gt;Let me give you three real fails from last month.&lt;/p&gt;

&lt;p&gt;A security role tagged as AI jobs. A Unity client role tagged as backend jobs. A JavaScript role tagged as Java jobs.&lt;br&gt;
All three came from our old keyword rules, and all three were live on the site until someone emailed me to complain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7vum32m4rco17h6doz8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7vum32m4rco17h6doz8p.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keywords are fast and cheap. They have no idea what a job actually means. An LLM alternative built for structured decisions does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Jev? A 60 second primer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Jev is the first public System One Model from TypeSafe AI, a San Francisco AI lab that came out of stealth on September 15, 2026 with 40M in seed funding.&lt;/p&gt;

&lt;p&gt;Founders are Diogo Almeida (ex OpenAI engineer who co wrote core ChatGPT training techniques), plus Erik Gafni and Sasha Sheng. The official announcement is here: &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Introducing System One Models and Jev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The name System One comes from Daniel Kahneman Thinking, Fast and Slow. Fast System 1 intuition vs slow System 2 reasoning. Jev is named after economist William Stanley Jevons.&lt;/p&gt;

&lt;p&gt;Unlike a large language model that chats and generates text, Jev is built for AI decision making inside software. You send a state plus typed questions. You get back typed, calibrated decisions in JSON that code can use directly. No chat, no hallucination in the classic sense, no long winded chain of thought.&lt;/p&gt;

&lt;p&gt;TypeSafe claims Jev is up to 193x faster and 444x cheaper than frontier LLMs on System One shaped tasks, with 70 to 500 ms end to end latency. Pricing on the direct TypeSafe API is 0.042 dollars per 1M input tokens, output tokens free. Current version in our logs is &lt;code&gt;jev-1.13.0&lt;/code&gt; with alias &lt;code&gt;jev-latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three primitives, and we use all three for job board AI classification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Noul: yes or no probability. Returns a noul float from 0 to 1. Ideal for binary checks like is_frontend, is_backend, is_java, is_ai, plus benefits and region gates.&lt;/li&gt;
&lt;li&gt;Choice: pick one option from up to 255 options. Returns choice plus full probabilities plus confidence. We use it for seniority classification and work model detection: fully_remote vs hybrid vs onsite.&lt;/li&gt;
&lt;li&gt;Score: rate on an ordered scale of 2 to 10 levels. Returns score plus legend plus probabilities plus confidence. Useful for severity, urgency, or ranking style judgments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions run in parallel and in isolation against the same state. Adding questions barely changes response time. That parallel sampler plus the training method TypeSafe calls Reinforcement Learning for Calibrated Decisions (RLCD) is why confidence is actually usable for branching in code.&lt;/p&gt;

&lt;p&gt;You can call Jev three ways: direct via &lt;code&gt;POST https://api.typesafe.ai/v1/systemone&lt;/code&gt; with the TypeSafe SDK, via Cloudflare Workers AI as typesafe/jev with env.AI.run, or via Vercel AI Gateway as typesafe-ai/jev. We run it via Cloudflare Workers AI, which keeps the call inside Cloudflare network and fits our Cloudflare hosted Next.js stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqx9fbyk3xu2ethea7kp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqx9fbyk3xu2ethea7kp6.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one picture: keyword job classification fails
&lt;/h2&gt;

&lt;p&gt;Before, ingest looked like this:&lt;/p&gt;

&lt;p&gt;Feed, keywords, save to database, show on site&lt;/p&gt;

&lt;p&gt;If the title had java in it, it went to the Java jobs board. Even if it was JavaScript. If the description mentioned AI once, like Copilot is a plus, it went to AI jobs. Even if it was a normal support role.&lt;br&gt;
Benefits extraction was worse. Real perks like 22 days PTO or 401k with 4 percent match often sit at the very bottom of the post, past 6000 characters, in a What We Offer block. Regex never saw it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frr4f6joddvw1k8t4xo7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frr4f6joddvw1k8t4xo7o.png" alt=" " width="559" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How we wired the Jev model into&amp;nbsp;ingest
&lt;/h2&gt;

&lt;p&gt;One fan out call per posting carries every question. Sites, seniority, regions, work model, benefits. Salaries stay on regex by design. Money needs exact extraction, not judgment.&lt;/p&gt;

&lt;p&gt;State we send: truncated title plus company plus location plus full description plus an isolated benefits section. Questions we send: 4 site Nouls, 1 seniority Choice, 8 region Nouls, 1 work model Choice, 13 benefits Nouls. All built in one file so prompts are easy to review.&lt;br&gt;
Response we get: jev-1.13.0 answers with noul, choice, probabilities, confidence, plus usage with input and output tokens. Every method returns null when Jev is unconfigured or errors, so callers fall back to keywords. Jev never breaks ingest.&lt;br&gt;
The trick: shadow mode first, enforce mode&amp;nbsp;later&lt;br&gt;
I did not want AI job classification to break the site. So we shipped in two modes.&lt;/p&gt;

&lt;p&gt;Step 1: Shadow mode, the default. JEV runs on a small sample per feed, usually 25 posts. It writes its raw answers to an audit field on the job, and writes a log row with what it decided vs what keywords decided. It changes nothing on the site. We just watch and compare.&lt;br&gt;
Step 2: Enforce mode, only when confident. When we turn it on, confident structured decisions can update the row. Below the bar, keywords win and we only log the disagreement.&lt;/p&gt;

&lt;p&gt;Each field has its own calibrated confidence bar, tuned on real labeled data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sites: low bar. The judge runs underconfident here.&lt;/li&gt;
&lt;li&gt;Regions: medium bar. Stricter than keywords.&lt;/li&gt;
&lt;li&gt;Work model: fill empty values easily, overwrite existing values only when very sure. A borderline call once flipped a correct hybrid to onsite, so overwrites need a higher bar.&lt;/li&gt;
&lt;li&gt;Seniority: only fills when keywords found nothing. Title plus years of experience still beats title alone.&lt;/li&gt;
&lt;li&gt;Benefits: prune only when sure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fail safe rules we kept for this LLM alternative:&lt;/p&gt;

&lt;p&gt;No token, no call, keywords keep working.&lt;br&gt;
Timeout after 8 seconds, one retry for transient errors, then skip.&lt;br&gt;
Only runs on posts that already passed dedupe, so we never pay for duplicates.&lt;br&gt;
Concurrency capped at 5, so mail and ingest never stall.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12qsp9ah3y75spfvpp82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12qsp9ah3y75spfvpp82.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two small details that fixed big AI classification errors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Read the full post for benefits, not the snippet. Snippets are often 500 characters of company boilerplate with zero signal. The perks live at the bottom. We now pass the full text, isolate the benefits section, and let only the benefits Noul questions read that section. Everything else reads the full description.&lt;br&gt;
Result: we stopped voting confident No on real perks like wellness stipend, RRSP match, and vacation just because we truncated too early.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Location counts as much as prose for geo classification. Remote US in the location field is stronger than three paragraphs of generic text. Same for Poland or Remote US. Our region Nouls now weigh location heavily. A bare Remote with no place named means worldwide, not Europe.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6f2f2gto1atwkllq6asn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6f2f2gto1atwkllq6asn.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for remote job&amp;nbsp;seekers
&lt;/h2&gt;

&lt;p&gt;This is why you should care if you just want a remote job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend jobs, Backend jobs, Java jobs, and AI jobs boards are cleaner. Fewer cross posts.&lt;/li&gt;
&lt;li&gt;Remote region filters actually mean something. Europe remote jobs means Europe, not worldwide with the word Europe somewhere in the footer.&lt;/li&gt;
&lt;li&gt;Remote work model is clearer. Fully remote vs hybrid vs onsite, based on both location and description.&lt;/li&gt;
&lt;li&gt;Remote benefits badges show up more often, and with fewer false positives. No more stock options tag for a diversity statement that contains the word equity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Try it: open any board, filter for salary plus region plus seniority, save that filter, and subscribe. You will feel the difference in the Tue and Sun digest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb74m5wjsxvpm5hr6wtbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb74m5wjsxvpm5hr6wtbs.png" alt=" " width="800" height="1433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for us as builders of an AI job&amp;nbsp;board
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every classified posting keeps its raw Jev answers as JSON in the database, with model name jev-1.13.0 and timestamp. We can re derive bars later without re paying for calls.&lt;/li&gt;
&lt;li&gt;Every call writes a queryable log row: board, title, latency in ms, input tokens, output tokens, what was applied vs only logged, errors. No more CSV dumps.&lt;/li&gt;
&lt;li&gt;Ingest logs now show both sides: keywords said X, Jev structured decision said Y. That made tuning the bars a 30 minute job instead of a debate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How we keep AI classification honest: /audit-jev
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrwvz3o86jybko1o7qxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrwvz3o86jybko1o7qxx.png" alt=" " width="780" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting Jev live was the easy part. Trusting it every day is the real work.&lt;/p&gt;

&lt;p&gt;We built a repeatable command called /audit-jev. Usage is simple: /audit-jev 100 audits the last 100 ingest classifications. It runs read only on prod, then forces fixes plus tests plus a changelog entry.&lt;/p&gt;

&lt;p&gt;Here is what it does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the last N ingest rows. It queries the JevLog table for scope ingest, ordered by newest first. For each row it looks at raw Noul plus Choice answers with confidences, plus what keywords said at the time, plus which fields enforce actually overwrote.&lt;/li&gt;
&lt;li&gt;Replay against the real Job rows. For suspicious rows it fetches the posting in chunks, because benefits tails sit past 5500 characters. Then it checks per area: benefits prunes, site writes, work model overwrites, region fills, seniority fills.&lt;/li&gt;
&lt;li&gt;Fix root causes, not symptoms. We prefer tightening keywords over lowering Jev confidence bars. Every fix gets regression tests, then a full test run plus typecheck.&lt;/li&gt;
&lt;li&gt;Append a changelog entry, every time. Even when nothing was wrong. Each entry lists scope, issues with real examples, fixes with file paths, tests, and a watchlist. Past entries are never rewritten, and a new fix is never allowed to silently remove an old fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This audit loop already paid off for our System One Model rollout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entry 1 found truncation blindness where What We Offer tails were invisible, plus pay equity text tagged as stock equity, plus hybrid text leaking into flexibility, plus a marginal work model overwrite flipping hybrid to onsite.&lt;/li&gt;
&lt;li&gt;Entry 2 verified those fixes still hold on a fresh mix of 100 rows, then found new substring bugs like intuition matching tuition inside a URL, plus engineering async matching flexibility, plus Portuguese titles like Senior and Pleno missed by keywords.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some excerpt of our audit-jev changelog&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  CHANGELOG
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Entry 1–2026–09–21, last 100 ingest rows
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scope: 100 rows, breezy-only boards (ontrac 18, ismira 13, social-discovery 11, kimmel 11, zendar 7,&amp;nbsp;…). Wrote counters (inflated by no-op re-writes, see fix 6): seniority 84, workModel 67, sites 39, benefits:prune 35, geoRegions 20, sites:prune 1.&lt;/li&gt;
&lt;li&gt;Issues found:&lt;/li&gt;
&lt;li&gt;Truncation blindness: &lt;code&gt;enrichWithJev&lt;/code&gt; sliced content to 6000 chars, &lt;code&gt;classifyPosting&lt;/code&gt; isolated benefits from the slice - "What We Offer" tails at ~5500–8800 chars invisible to JEV → confident-False prunes of true perks (Egym Wellpass/Wellness Pass, Group RRSP match, 401(K) match, "4 weeks of vacation", "Flexible time off", medical/dental/vision). Same sentence opposite verdicts across jobs.&lt;/li&gt;
&lt;li&gt;Equity pay-equity FP: "…depending on experience and equity" tagged equity comp (zendar salary line).&lt;/li&gt;
&lt;li&gt;Flexibility workModel leak: &lt;code&gt;hybrid (work|model)&lt;/code&gt; + &lt;code&gt;work from anywhere&lt;/code&gt; emitted flexibility for hybrid-3-days and LATAM-remote rows JEV rejects.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you take one idea from this JEV post, take this one. Do not ship AI classification without a one command audit that anyone can rerun.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to copy this TypeSafe Jev&amp;nbsp;pattern
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep keywords as fallback. Never let a System One Model break ingest.&lt;/li&gt;
&lt;li&gt;Start in shadow mode. For the first few weeks I let it watch and take notes without touching anything on the site.&lt;/li&gt;
&lt;li&gt;Use per field confidence bars, not one global bar. Sites and benefits behave very differently.&lt;/li&gt;
&lt;li&gt;Isolate the section that matters. For us that was benefits at the bottom.&lt;/li&gt;
&lt;li&gt;Audit every decision with one command like /audit-jev. You will thank yourself when you retune Noul and Choice thresholds.&lt;/li&gt;
&lt;li&gt;Cap concurrency and timeouts. Even a 70 ms model should never stall your pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full stack here: NestJS job aggregator, Prisma plus MySQL, Next.js on Cloudflare, Jev model via Cloudflare Workers AI typesafe/jev with gateway support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want Part 3? I can go deep on remote benefits extraction, or on instant company alerts plus Seeker Pro. Tell me in the comments which one you want next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By Angel, builder of remotefrontendjobs.com, remotebackendjobs.com, and remotejavajobs.com.&lt;br&gt;
&lt;em&gt;Topics covered: Jev, TypeSafe AI, System One Models, System One Model, Jev model, jev-1.13.0, jev-latest, Noul, Choice, Score, calibrated decisions, structured decisions, LLM alternative, Cloudflare Workers AI, Vercel AI Gateway, AI job classification, AI job board, remote jobs, remote frontend jobs, remote backend jobs, remote Java jobs, benefits extraction, work model detection, seniority classification.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>jev</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Anatomy of a fast Next.js job board</title>
      <dc:creator>Angel Nikolov</dc:creator>
      <pubDate>Mon, 06 Dec 2021 14:42:28 +0000</pubDate>
      <link>https://dev.to/angelnikolov/anatomy-of-a-fast-nextjs-job-board-958</link>
      <guid>https://dev.to/angelnikolov/anatomy-of-a-fast-nextjs-job-board-958</guid>
      <description>&lt;h2&gt;
  
  
  Anatomy of a fast Next.js job board
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9EFso5Rw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/9654/0%2AKdlHNuIrov5kmffD" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9EFso5Rw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/9654/0%2AKdlHNuIrov5kmffD" alt="Photo by [ian dooley](https://unsplash.com/@sadswim?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="880" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hello everyone! I’ve been working remotely for quite some time now and I also have a couple of failed side-projects behind me. Recently, I realised that I should probably invest time in new projects only if they made practical sense for me.&lt;br&gt;
Then it hit me that the very thing that made it possible for me to work on side-projects was actually the thing which made sense for me to optimise and put more effort and ideas in. And that was remote work. One issue with remote work kept coming at me and that was — finding my new work place. I’ve used many different job boards, but all of them had different filters, job selection, mailing lists and etc. which obviously wasn’t ideal.&lt;/p&gt;

&lt;p&gt;That’s when I figured that maybe it will be nice to have all of the most used job boards aggregated and presented to users in a nice, fast and efficient way. I purchased several domains which I used to further refine the choice of the user by job categories.&lt;br&gt;
&lt;a href="https://www.remotefrontendjobs.com/"&gt;https://www.remotefrontendjobs.com&lt;/a&gt; and &lt;a href="https://www.remotebackendjobs.com/"&gt;https://www.remotebackendjobs.com&lt;/a&gt; are now listing thousands of jobs, aggregated from more than 14 different sources (all linking back). People could also subscribe to weekly tailored notifications for new jobs (they can also specify if they are only interested in jobs with salaries specified).&lt;/p&gt;

&lt;h3&gt;
  
  
  In this blog post I will go through how I created a fast, beautiful and tailored job-hunting experience running on Next.js and Vercel ❤️.
&lt;/h3&gt;

&lt;p&gt;To keep you interested, these are the tools that I’ve used for the first version of my job board which I will go through in this article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--b01yXUtB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2022/1%2AALHriJeBxipJxzNVPb-waA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--b01yXUtB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2022/1%2AALHriJeBxipJxzNVPb-waA.png" alt="React, Next.js, Vercel, Fuse.js and Styled Components" width="880" height="77"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I started out, I wanted to pick the set of tools which would enable me to create a fully functioning job board in a quick and predictable manner. Initially, this was a really quick and neat idea in my head which would’ve taken me no more than a couple of hours. Basically, what I had in mind was a simple page with a list of jobs and a search bar on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9K1GZJ1D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2632/1%2AG2BiBMyXrRCzuACcg0nPPQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9K1GZJ1D--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2632/1%2AG2BiBMyXrRCzuACcg0nPPQ.png" alt="" width="880" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what I started with was a simple SSG (&lt;a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation"&gt;Static site generation getStaticProps&lt;/a&gt;) page in Next.js and a couple of &lt;a href="https://vercel.com/docs/concepts/functions/introduction"&gt;Serverless functions &lt;/a&gt;which I can use to get the actual jobs. Whenever a user would navigate to my page, they would load the list below, which would be generated at build time by scraping some job boards.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
&lt;strong&gt;There are a couple of important things to note in the code above.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I am using &lt;a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation"&gt;GetStaticProps&lt;/a&gt; to render this page at build time. This basically renders the whole application on the server and outputs a plain old and heavily optimized HTML pages which should load as fast as possible without any overhead&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All of the components this page is composed of are simple presentational components using styled-components for styling. Adding styled-components to a Next.js project is rather simple and it’s well explained &lt;a href="https://styled-components.com/docs/advanced#nextjs"&gt;here&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I use  and the useAmp hook for AMP-enabled images when building for AMP which Next.js &lt;a href="https://nextjs.org/docs/advanced-features/amp-support/introduction"&gt;supports out of the box&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next Image will not work there since AMP has really strict constraints on what can be used, and we can’t also use &lt;a href="https://nextjs.org/docs/api-reference/next/image"&gt;next/image&lt;/a&gt; yet since we can’t possible list out all possible &lt;a href="https://nextjs.org/docs/basic-features/image-optimization#domains"&gt;remote image domains&lt;/a&gt; for our jobs since they come from many different sources which could change at any moment. We will explain how we solved this issue in the next blog post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fetcher is a class which houses all of our data scraping during build time as well as our serverless functions which will be used when users use the search-bar. Here’s how it looks like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;

What we basically do above is pretty simple — we import all feed functions, loop through them and call each one of them with a searchables and filter parameters. Searchables is basically an environment variable for search terms which I will always use for different job sites like &lt;a href="http://www.remotefrontendjobs.com"&gt;www.remotefrontendjobs.com&lt;/a&gt; and &lt;a href="http://www.remotebackendjobs.com"&gt;www.remotebackendjobs.com&lt;/a&gt;. So for the first one I’d pass stuff like frontend,js,javascript,angular,react and etc.
On the other hand, filter will be used when someone uses the search-bar and wants to search for something specific. For the filtering, I use a really neat fuzzy search library called &lt;a href="https://fusejs.io/"&gt;fuse.js&lt;/a&gt;. In the end, we also filter all jobs which was posted in the last 20 days, so we don’t clutter our board with old jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;We also export a simple serverless function that uses the same fetcher which will be utilized for our runtime search. This function will be automatically served for us in development by using next dev or next start but it will also be deployed into multiple regions if used in &lt;a href="https://vercel.app/"&gt;Vercel&lt;/a&gt;. This only proves that the experience of using Next.js is simply magical 🥰.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For every job board, I use the same feed function to fetch specific data segments of a remote job&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
So whenever I want to introduce a new job board to the mix I just implement this interface and then I let Next and Vercel (in production) handle filling up my pages with data whenever I build the site.&lt;br&gt;
For example, the feed function for Stackoverflow looks like this:&lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Here we use rss-parser to fetch the public &lt;a href="http://stackoverflow.com/jobs/feed"&gt;Stackoverflow jobs rss feed&lt;/a&gt;, transform it into a nice data model and return it to our page.

&lt;p&gt;In the initial version of the project I had 7 different job board feed functions exported like this&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
You can see how easy it is to introduce a new job board to the aggregated mix now.

&lt;h2&gt;
  
  
  Recap and what comes next
&lt;/h2&gt;

&lt;p&gt;So let’s recap and take a look at what we have right now.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;On every build I will create a page with the first 25 jobs found from an aggregated list of data I retrieve from multiple websites.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I have a simple  component which calls into the exported api.ts serverless function, updates the state of  and displays the job offers. We also have an infinite scrolling functionality which does the same API call to load more jobs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The serverless API call hooks into the same process of getting jobs data as during the actual build.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I guess you can see that we have a couple of issues here.&lt;/p&gt;

&lt;p&gt;The first set of problems are related to the way we retrieve jobs for the static site generation. How do we update those 25 jobs on our static page? After all, I want to provide my site’s visitors with fresh new jobs as they come in. Do I make a new build everytime I want to update the jobs? And if I do, how do I know when? Would that be expensive? &lt;em&gt;Vercel has a pretty nice free quota, but still if this projects would grow, maybe I will hit it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then we have a different set of issues related to the runtime experience on our page. Right now when a user searches or loads more jobs, I go through the whole scraping process all over again. This means literally scrapping all feeds and extracting data from them. This is obviously not ideal since it’s generally a slow process and while one might think it can be solved easily with cache, then we arrive at a whole new set of cache-related problems, like — when to invalidate the cache. 😵&lt;/p&gt;

&lt;p&gt;I have solved the first set of problems from above by &lt;strong&gt;utilising &lt;a href="https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration"&gt;Incremental Static Regeneration&lt;/a&gt; to regenerate the home page at a specific time interval, but only when it’s used.&lt;/strong&gt; The second issues required a smarter, more scalable approach which would not only let me &lt;strong&gt;provide a faster experience to my users, but also a smarter and more fine-grained one&lt;/strong&gt;. That required introducing my own database (mysql) by using a really cool and well-adopted now ORM called &lt;a href="https://www.prisma.io/"&gt;Prisma&lt;/a&gt;. Where I am hosting the database and how I am managing and updating it will come with my next article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This blogpost only covered the initial stages of the remote job aggregator and my next blog posts will build on top of that so you get a clear picture on what decisions were made in terms of performance and UX optimisations and how all of that was achieved. The application right now doesn’t look anything like what was shown above both in terms of design and functionality, and you can take a look at the latest versions at &lt;a href="http://www.remotefrontendjobs.com"&gt;www.remotefrontendjobs.com&lt;/a&gt; and &lt;a href="http://www.remotebackendjobs.com."&gt;www.remotebackendjobs.com.&lt;/a&gt; You can use these to find the perfect remote job for you and subscribe for weekly new jobs emails (you can specify that you want jobs with salaries only).&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Improve your Angular app performance by using this simple Observable cache decorator 🎉
</title>
      <dc:creator>Angel Nikolov</dc:creator>
      <pubDate>Mon, 15 Jul 2019 12:21:04 +0000</pubDate>
      <link>https://dev.to/angelnikolov/improve-your-angular-app-performance-by-using-this-simple-observable-cache-decorator-1bng</link>
      <guid>https://dev.to/angelnikolov/improve-your-angular-app-performance-by-using-this-simple-observable-cache-decorator-1bng</guid>
      <description>&lt;p&gt;When we were about to finish development of our applications in SwiftViews we noticed a pattern in all our data-fetching user flows. In spite of the fact that the apps are all data-driven and looked really dynamic, what really was changing in the same user session wasn’t that much, but we were making http requests for new content regardless.&lt;br&gt;
The easiest solution was caching&lt;br&gt;
Caching where? On the server? We already have that, but this doesn’t stop all our apps hitting our services and thus — increasing the load on them.&lt;br&gt;
Maybe we could use service worker since it allows caching API calls?&lt;br&gt;
Yes, that was one of the options since it already has a pretty nice integration with Angular and allowed for a simple solution to selectively cache resources and APIs. However, what we wanted is to be able to not only choose what to cache, but also when to do it.&lt;br&gt;
Just to give you a quick example — we basically wanted to cache all API call results for the page below, but only if we were certain that the data source did not change in some way.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx5df5ibqm2u4nqndgtz1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx5df5ibqm2u4nqndgtz1.png" alt="Inventory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since this is my personal inventory page, the only way it can actually change is if I add an item from this platform (currently the only way to do so), so I have the information about when this page will change and how long I can return cached content for.&lt;br&gt;
We figured the best way to apply this caching in a selective and configurable way will be to use what the platform already gives us&lt;br&gt;
Since we are using Angular and Typescript ❤️, and all our data calls go through RxJs, we figured that we can create a Cache Observable decorator, which we’d use to only give the caching power to certain methods.&lt;br&gt;
So, before, this was our method which called our server to get the products on the page above:&lt;/p&gt;

&lt;p&gt;and that became:&lt;/p&gt;

&lt;p&gt;Notice that the @Cacheable() has been applied just to the method we wanted and was also passed cacheBusterObserver which is basically our Subject-based mechanism to tell this exact decorated method to relieve all its caches, when any value is emitted in that stream.&lt;br&gt;
For example, the method below will “cache-bust” the caches of the method above, if the Observable it returns emits a value 😵&lt;/p&gt;

&lt;p&gt;This might be the most complex example we have but we also have other configurations implemented like maxCacheCount, maxAge, slidingExpiration and custom cache deciders and resolvers which give us a fine-grain control on what, when and how to cache. See those in the README file.&lt;br&gt;
In the two gifs below, you can see the difference between our app’s performance without and with caching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.loom.com/share/71a03e4cd478407fa53f131fc112e09a" rel="noopener noreferrer"&gt;https://www.loom.com/share/71a03e4cd478407fa53f131fc112e09a&lt;/a&gt;&lt;br&gt;
No Cacheable&lt;br&gt;
The cacheable decorator is not applied yet, so every page load will actually fetch data from the server&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.loom.com/share/380a1fa93a9c42d0a5adaa987cd51efb" rel="noopener noreferrer"&gt;https://www.loom.com/share/380a1fa93a9c42d0a5adaa987cd51efb&lt;/a&gt;&lt;br&gt;
Cacheable decorator applied 🎉&lt;br&gt;
The cacheable decorator is applied and we can see that all consecutive page loads are faster. Also the user profile page loads instantly, because we have already called the /user/{id}endpoint on the inventory page. Also, the cache busts after we add a new item so our user gets fresh data&lt;br&gt;
So, to sum up, this simple decorator allowed us to:&lt;br&gt;
Selectively cache observable methods (not just endpoints, but maybe also computation-heavy calculations in streams)&lt;br&gt;
Improve the performance of our app, without any business logic refactoring (thanks TS decorators ❤️)&lt;br&gt;
Greatly reduce the load on our servers&lt;br&gt;
If you want to use this decorator in your project, just install it from NPM!&lt;br&gt;
npm install ngx-cacheable&lt;br&gt;
If you have any questions about it or want to contribute, don’t mind opening a pull request @github or commenting below.&lt;br&gt;
Also, if you liked the application, please register and build your own inventory @ swiftviews.&lt;br&gt;
Thank you! 🐦&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>rxjs</category>
      <category>node</category>
    </item>
  </channel>
</rss>
