I Built a Full-Stack Invoice App from Scratch. Here's the Complete Breakdown

Published: (May 1, 2026 at 05:47 PM EDT)
2 min read
Source: Dev.to

Source: Dev.to

Most invoice tools are either too expensive or too complicated. I built my own in one week, deployed it live, and documented every technical decision and lesson learned.

Live Demo & Source Code

Tech Stack

  • Frontend: React
  • Backend: Node.js with Express
  • Database: PostgreSQL
  • Deployment: Frontend on Vercel, backend on Render

Authentication Middleware

Every protected route runs through a single middleware before reaching the controller.

// auth.js
const auth = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

Clean, reusable, and protects every route with one function.

Automatic Database Migrations

The server creates tables automatically on startup, so anyone who clones the repo gets a working database in seconds.

// server.js
migrate.createTables().then(() => {
  app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
});

Global Axios Interceptor

Instead of attaching the JWT token to each request manually, an interceptor adds it globally.

// api.js
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Write it once, forget about it.

Deployment Gotchas

  1. Render SSL for PostgreSQL – Add rejectUnauthorized: false to your pg Pool config in production, otherwise connections fail.
  2. CORS configuration – Explicitly list your Vercel domain; a wildcard does not work with credentials.
  3. JWT expiry values – Environment variables must be strings. If the value is undefined, jwt.sign throws a silent error. Provide a fallback value.

These issues cost me two hours; now they cost you nothing.

Dashboard Overview

The dashboard displays:

  • Total invoices
  • Total clients
  • Revenue collected
  • Outstanding balance

All figures are calculated from live database queries on every load—no fake data or hard‑coded numbers.

Additional Features

  • PDF export for invoices
  • Email delivery via Nodemailer
  • Recurring invoices
  • Payment gateway integration

Conclusion

If this breakdown helped you, consider starring the repository on GitHub and leaving a comment.

Tags: javascript, node, react, webdev

0 views
Back to Blog

Related posts

Read more »