DEV Community

Cover image for Build a Side Hustle Finder Web App with Next.js A Practical Step‑by‑Step Guide Inspired by VectricEarn
Obed Avorlenu
Obed Avorlenu

Posted on

Build a Side Hustle Finder Web App with Next.js A Practical Step‑by‑Step Guide Inspired by VectricEarn

TL;DR – In this tutorial, you’ll build a full‑stack web app that helps users discover side hustles, save favourites, and follow practical guides. The content is inspired by the kind of actionable advice you'd find on VectricEarn – a website that covers personal finance, remote jobs, side hustles, investing, career growth, and scholarships for Africans. Your finished app can become a portfolio piece, a community tool, or even a monetisable product.


Why This Project?

Across Africa, young people are looking for practical ways to earn extra income, grow their careers, and build wealth. Resources like VectricEarn provide valuable guides on:

· Personal Finance – Budgeting, saving, and investing in the African context
· Remote Jobs & Freelancing – Legitimate work opportunities that pay in foreign currency
· Side Hustles – Step‑by‑step guides to earning outside your 9‑to‑5
· Investing & Wealth Building – Stocks, ETFs, real estate, and crypto explained simply
· Career Growth – Salary negotiation, promotions, and in‑demand skills
· Scholarships & Opportunities – Fully funded programs and grants

This Side Hustle Finder app complements that mission by helping users:

· Browse hustle ideas filtered by category, startup cost, and difficulty
· Read step‑by‑step guides for each hustle
· Save favourite hustles to a personal dashboard
· Track estimated income and progress

What’s in it for you?
Building this app teaches you:

· Full‑stack development with Next.js (App Router + API routes)
· Database modelling with Prisma and SQLite
· Authentication with NextAuth.js
· Modern frontend with Tailwind CSS
· How to turn a real‑world problem into a software solution

And you can earn from it too: use it as a portfolio project to land freelance gigs, or package it as a SaaS for local entrepreneurs.


Prerequisites

· Basic knowledge of JavaScript/TypeScript and React
· Node.js 18+ installed
· A code editor (VS Code recommended)
· Familiarity with the command line


Step 1: Set Up Your Next.js Project

# Create a new Next.js project with TypeScript and Tailwind CSS
npx create-next-app@latest side-hustle-finder --typescript --tailwind --app
cd side-hustle-finder

# Install dependencies
npm install @prisma/client prisma next-auth @types/next-auth bcryptjs
npm install -D @types/bcryptjs
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up the Database with Prisma

npx prisma init
Enter fullscreen mode Exit fullscreen mode

Edit prisma/schema.prisma:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  username  String   @unique
  email     String   @unique
  password  String   // hashed
  savedHustles SavedHustle[]
}

model Hustle {
  id        String   @id @default(cuid())
  title     String
  category  String
  startupCost String
  difficulty String
  description String
  steps     String   // JSON array of practical steps
  estimatedIncome String
  savedBy   SavedHustle[]
}

model SavedHustle {
  id        String @id @default(cuid())
  userId    String
  hustleId  String
  user      User @relation(fields: [userId], references: [id])
  hustle    Hustle @relation(fields: [hustleId], references: [id])

  @@unique([userId, hustleId])
}
Enter fullscreen mode Exit fullscreen mode

Set .env:

DATABASE_URL="file:./dev.db"
Enter fullscreen mode Exit fullscreen mode

Run migration:

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

Step 3: Seed the Database with Real Hustle Ideas

Create prisma/seed.ts (install ts-node as dev dependency first). These ideas are inspired by the practical guides you'd find on VectricEarn – covering side hustles that actually work in the African context.

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const hustles = [
  {
    title: "AI‑Powered Tutoring via WhatsApp",
    category: "Teaching",
    startupCost: "$0",
    difficulty: "Beginner",
    description: "Use ChatGPT + WhatsApp to teach subjects you already know.",
    steps: JSON.stringify([
      "Ask ChatGPT to create a lesson plan",
      "Design a flyer using Canva",
      "Share on WhatsApp status and groups",
      "Teach via voice notes and shared docs"
    ]),
    estimatedIncome: "$10‑$50 per student"
  },
  {
    title: "Canva + AI Micro‑Designs for Local Shops",
    category: "Design",
    startupCost: "$0",
    difficulty: "Beginner",
    description: "Create posters, menus, and flyers for local businesses using Canva and AI.",
    steps: JSON.stringify([
      "Find 5 small shops in your neighbourhood",
      "Use ChatGPT to write catchy slogans",
      "Design posters in Canva",
      "Show samples and offer 3 designs for $10"
    ]),
    estimatedIncome: "$10‑$30 per design"
  },
  {
    title: "Virtual Research Assistant",
    category: "Writing",
    startupCost: "$0",
    difficulty: "Intermediate",
    description: "Help students and organisations with research using ChatGPT.",
    steps: JSON.stringify([
      "Join student groups on Facebook/LinkedIn",
      "Offer to summarise papers or find references",
      "Use Google Docs to deliver summaries",
      "Polish AI output in your own voice"
    ]),
    estimatedIncome: "$20‑$50 per project"
  },
  {
    title: "Social Media Caption Micro‑Agency",
    category: "Marketing",
    startupCost: "$0",
    difficulty: "Intermediate",
    description: "Write captions and manage social media for local businesses.",
    steps: JSON.stringify([
      "Pick 3 local brands",
      "Ask ChatGPT for caption ideas",
      "Schedule posts with Meta Business Suite",
      "Offer management for $30‑$50/month"
    ]),
    estimatedIncome: "$50‑$150/month"
  },
  {
    title: "Remote Freelance Writing",
    category: "Writing",
    startupCost: "$0",
    difficulty: "Intermediate",
    description: "Write for international clients on platforms like Upwork and Fiverr.",
    steps: JSON.stringify([
      "Choose a niche (tech, finance, lifestyle)",
      "Build a portfolio with sample articles",
      "Create profiles on Upwork and Fiverr",
      "Start with smaller gigs to build reputation"
    ]),
    estimatedIncome: "$100‑$1000+ per month"
  }
];

async function main() {
  for (const h of hustles) {
    await prisma.hustle.upsert({
      where: { title: h.title },
      update: {},
      create: h,
    });
  }
}
main()
  .catch(e => console.error(e))
  .finally(() => prisma.$disconnect());
Enter fullscreen mode Exit fullscreen mode

Add seed script in package.json:

"prisma": {
  "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
Enter fullscreen mode Exit fullscreen mode

Run seeding:

npx prisma db seed
Enter fullscreen mode Exit fullscreen mode

Step 4: Set Up Authentication (NextAuth.js)

Create src/app/api/auth/[...nextauth]/route.ts:

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";

const prisma = new PrismaClient();

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;
        const user = await prisma.user.findUnique({
          where: { email: credentials.email }
        });
        if (!user) return null;
        const isValid = await bcrypt.compare(credentials.password, user.password);
        if (!isValid) return null;
        return { id: user.id, email: user.email, name: user.username };
      }
    })
  ],
  session: { strategy: "jwt" },
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.id = user.id;
      return token;
    },
    async session({ session, token }) {
      if (token?.id) session.user.id = token.id as string;
      return session;
    }
  },
  pages: { signIn: "/login" },
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Enter fullscreen mode Exit fullscreen mode

Step 5: Build API Routes

src/app/api/hustles/route.ts – GET all with filters:

import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const category = searchParams.get('category');
  const cost = searchParams.get('cost');
  const difficulty = searchParams.get('difficulty');

  const where: any = {};
  if (category && category !== 'All') where.category = category;
  if (cost) where.startupCost = cost;
  if (difficulty) where.difficulty = difficulty;

  const hustles = await prisma.hustle.findMany({ where });
  return NextResponse.json(hustles);
}
Enter fullscreen mode Exit fullscreen mode

src/app/api/hustles/[id]/route.ts – GET single:

import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function GET(req: Request, { params }: { params: { id: string } }) {
  const hustle = await prisma.hustle.findUnique({ where: { id: params.id } });
  if (!hustle) return new NextResponse('Not found', { status: 404 });
  return NextResponse.json(hustle);
}
Enter fullscreen mode Exit fullscreen mode

src/app/api/saved/route.ts – POST save, DELETE unsave (protected):

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '../auth/[...nextauth]/route';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

export async function POST(req: Request) {
  const session = await getServerSession(authOptions);
  if (!session) return new NextResponse('Unauthorized', { status: 401 });
  const { hustleId } = await req.json();
  await prisma.savedHustle.create({
    data: { userId: session.user.id, hustleId }
  });
  return NextResponse.json({ success: true });
}

export async function DELETE(req: Request) {
  const session = await getServerSession(authOptions);
  if (!session) return new NextResponse('Unauthorized', { status: 401 });
  const { hustleId } = await req.json();
  await prisma.savedHustle.delete({
    where: { userId_hustleId: { userId: session.user.id, hustleId } }
  });
  return NextResponse.json({ success: true });
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Build the Frontend Pages

Homepage (src/app/page.tsx)

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { Hustle } from '@prisma/client';

export default function Home() {
  const { data: session } = useSession();
  const [hustles, setHustles] = useState<Hustle[]>([]);
  const [categories, setCategories] = useState<string[]>([]);

  useEffect(() => {
    fetch('/api/hustles')
      .then(res => res.json())
      .then(data => {
        setHustles(data);
        const cats = data.map((h: Hustle) => h.category);
        setCategories(['All', ...new Set(cats)]);
      });
  }, []);

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-4">Find Your Next Side Hustle 💰</h1>
      <p className="text-lg mb-6">Discover legitimate ways to earn extra income — built for the African context.</p>
      <div className="flex gap-4 mb-6">
        <select className="border p-2 rounded">
          <option>All Categories</option>
          {categories.map(c => <option key={c}>{c}</option>)}
        </select>
        <select className="border p-2 rounded">
          <option>All Costs</option>
          <option>$0</option>
          <option>$10-50</option>
          <option>$50+</option>
        </select>
      </div>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {hustles.map(h => (
          <div key={h.id} className="border rounded-lg p-4 shadow">
            <h2 className="text-xl font-semibold">{h.title}</h2>
            <div className="flex gap-2 my-2">
              <span className="bg-gray-200 px-2 py-1 rounded text-sm">{h.category}</span>
              <span className="bg-blue-100 px-2 py-1 rounded text-sm">{h.difficulty}</span>
              <span className="bg-green-100 px-2 py-1 rounded text-sm">{h.startupCost}</span>
            </div>
            <p className="text-gray-600">{h.description.slice(0, 100)}...</p>
            <div className="mt-4 flex justify-between items-center">
              <Link href={`/hustle/${h.id}`} className="text-blue-600 hover:underline">View Guide</Link>
              {session && (
                <button
                  onClick={async () => {
                    await fetch('/api/saved', { method: 'POST', body: JSON.stringify({ hustleId: h.id }) });
                  }}
                  className="bg-green-500 text-white px-3 py-1 rounded"
                >
                  Save
                </button>
              )}
            </div>
            <div className="mt-2 text-sm text-gray-500">Est. income: {h.estimatedIncome}</div>
          </div>
        ))}
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Detail Page (src/app/hustle/[id]/page.tsx)

import { PrismaClient } from '@prisma/client';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';

const prisma = new PrismaClient();

export default async function HustlePage({ params }: { params: { id: string } }) {
  const session = await getServerSession(authOptions);
  const hustle = await prisma.hustle.findUnique({ where: { id: params.id } });
  if (!hustle) notFound();

  const steps = JSON.parse(hustle.steps) as string[];

  return (
    <main className="container mx-auto p-4">
      <nav className="text-sm mb-4"><Link href="/" className="text-blue-600">Home</Link> / {hustle.title}</nav>
      <h1 className="text-3xl font-bold">{hustle.title}</h1>
      <div className="flex gap-2 my-3">
        <span className="bg-gray-200 px-2 py-1 rounded">{hustle.category}</span>
        <span className="bg-blue-100 px-2 py-1 rounded">{hustle.difficulty}</span>
        <span className="bg-green-100 px-2 py-1 rounded">Startup: {hustle.startupCost}</span>
        <span className="bg-yellow-100 px-2 py-1 rounded">Est. income: {hustle.estimatedIncome}</span>
      </div>
      <p className="text-lg">{hustle.description}</p>
      <h2 className="text-2xl font-semibold mt-6">Step‑by‑Step Guide</h2>
      <ol className="list-decimal list-inside space-y-2 mt-2">
        {steps.map((step, i) => <li key={i}>{step}</li>)}
      </ol>
      {session ? (
        <button
          onClick={async () => {
            await fetch('/api/saved', { method: 'POST', body: JSON.stringify({ hustleId: hustle.id }) });
          }}
          className="mt-6 bg-green-500 text-white px-4 py-2 rounded"
        >
          Save to Dashboard
        </button>
      ) : (
        <Link href="/login" className="mt-6 inline-block text-blue-600">Login to save</Link>
      )}
      <Link href="/" className="mt-4 inline-block ml-4 text-gray-600">Back to all</Link>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dashboard (src/app/dashboard/page.tsx) – protected

import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { PrismaClient } from '@prisma/client';
import Link from 'next/link';
import { redirect } from 'next/navigation';

const prisma = new PrismaClient();

export default async function Dashboard() {
  const session = await getServerSession(authOptions);
  if (!session) redirect('/login');

  const saved = await prisma.savedHustle.findMany({
    where: { userId: session.user.id },
    include: { hustle: true }
  });

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-2xl font-bold">My Saved Hustles</h1>
      {saved.length === 0 ? (
        <p>You haven't saved any hustles yet. <Link href="/" className="text-blue-600">Browse all</Link>.</p>
      ) : (
        <ul className="space-y-4 mt-4">
          {saved.map(({ hustle }) => (
            <li key={hustle.id} className="border p-4 rounded flex justify-between items-center">
              <Link href={`/hustle/${hustle.id}`} className="text-xl font-medium">{hustle.title}</Link>
              <div>
                <span className="mr-4 text-sm">{hustle.estimatedIncome}</span>
                <button
                  onClick={async () => {
                    await fetch('/api/saved', { method: 'DELETE', body: JSON.stringify({ hustleId: hustle.id }) });
                  }}
                  className="bg-red-500 text-white px-3 py-1 rounded"
                >
                  Remove
                </button>
              </div>
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Login & Register Pages

Simple client‑side forms using next-auth/react – implement with standard forms and call signIn('credentials', { email, password }).


Step 7: Run Your App

npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000. Register, log in, explore, and save hustles.


How to Monetise This Project

· White‑Label it – Sell the app to local businesses, universities, or co‑working spaces as a tool for their communities.
· Offer Custom Content – Charge to add more hustles (like those covered on [Next.js and tailor the guides to specific regions.
· Build a Community – Add forums, success stories, and monetise through ads or premium listings.
· Showcase in Your Portfolio – Use it to land freelance or full‑time roles in web development.


Next Steps

· Add user‑submitted hustles (moderated) – this could turn into a crowdsourced library of practical guides.
· Implement income tracking – let users log their actual earnings to see progress.
· Integrate payment gateways for premium features.
· Improve filters with server‑side pagination and search.


Conclusion

You've built a full‑stack, production‑ready Next.js application that addresses a genuine need for Africans looking to earn extra income. The data and ideas used are drawn from real‑world opportunities – the kind you'd find on resources like VectricEarn, which provides practical guides on personal finance, remote jobs, side hustles, investing, career growth, and scholarships.

This project is not just a coding exercise; it's a potential revenue stream. Deploy it, iterate, and watch it grow.

Happy building – and happy earning! 🚀

Top comments (0)