Build a REST API in Node.js (Beginner-Friendly, Auth + Database)
Updated on February 07, 2026 14 minutes read
Career changes are exciting, but tech job requirements can feel intimidating. You'll see "API experience," "auth," "databases," and "deployment" listed everywhere, even for junior roles.
This guide is for adults switching into tech (or leveling up) who want one practical project that proves real backend skill. You'll build a REST API you can deploy, document, and confidently discuss in interviews.
You'll also get a timeline you can follow around work and life, plus a portfolio and first-job strategy. The goal isn't to collect tutorials. It's to ship a project that hiring teams recognize as job-ready.
Why a REST API project is a smart move for career changers
A REST API is one of the clearest "signals" you can show employers. It demonstrates you can build the systems that power real products, not just toy scripts.
In a single project, you practice routing, controllers, databases, authentication, error handling, and security. Those are the same concepts teams use every day on the job.
Even better, APIs are easy to evaluate. A recruiter or developer can test your endpoints in minutes, which increases your chances of getting a callback.
What you'll build
You'll build a Task Manager API because it's simple to understand but realistic to implement. The domain is familiar, so your effort goes into architecture and quality.
Your API will include authentication, a database, and full CRUD features. You'll also add validation, documentation, basic tests, and deployment.
If you prefer a different theme (habit tracker, notes app, booking system), you can swap the "Task" resource later. The core backend structure stays the same.
The beginner-friendly tech stack
There are many valid stacks, but this one is approachable and popular in real projects. It's also easy to explain in interviews.
- Node.js for the runtime
- Express for HTTP routing and middleware
- PostgreSQL for the database
- Prisma for schema, migrations, and queries
- JWT for authentication
- bcrypt for password hashing
- Zod for input validation
- Jest + Supertest for endpoint tests
This stack gives you career-grade skills without unnecessary complexity. It's a strong foundation for backend or full-stack roles.
What hiring teams want to see in a junior backend project
Most junior projects fail for one reason: they look like tutorial clones. Hiring teams don't need perfection, but they want evidence you understand the essentials.
They look for clear structure (routes, controllers, middleware), proper status codes, and sensible data modeling. They also care about secure authentication and clean error handling.
They also love anything that makes your project easy to review. A good README, deployed link, and simple API docs can be the difference between "skip" and "interview."
A realistic timeline you can follow
Your timeline should match your schedule, not your motivation on day one. Choose a pace you can sustain, and commit to finishing.

7-day sprint (fast track)
This is intense, but doable if you have focused time daily. You'll build essentials first, then polish quickly.
- Day 1 to 2: Setup + database + schema
- Day 3: Register/login + JWT
- Day 4: CRUD tasks + ownership checks
- Day 5: Validation + error handling
- Day 6: Docs + tests
- Day 7: Deploy + portfolio polish
3 to 4 week plan (most sustainable)
This is the best option for most career changers. You'll produce cleaner code, better documentation, and stronger confidence.
- Week 1: Setup, schema, migrations, basic server
- Week 2: Auth, middleware, CRUD endpoints
- Week 3: Validation, pagination, security improvements
- Week 4: Tests, docs, deployment, portfolio story + interview prep
The best timeline is the one you finish. Shipping a solid project beats starting three ambitious ones.
Step 1: Set up your Node.js project
Create a new folder and initialize npm. Keep the setup simple, but organized from the start.
mkdir task-manager-api
cd task-manager-api
npm init -y
Install core dependencies for an Express API. These handle routing, security basics, and environment variables.
npm i express cors dotenv jsonwebtoken bcrypt zod
npm i helmet morgan express-rate-limit
Install dev tools for a smoother workflow. Prisma manages your DB schema, and Jest/Supertest handles testing.
npm i -D nodemon prisma jest supertest
npm i @prisma/client
Add a clean folder structure so your repo looks professional. This also makes your code easier to explain in interviews.
mkdir -p src/{routes, controllers,middlewares, utils, schemas}
touch src/server.js .env
Add scripts to package.json so running the project is obvious.
{
"scripts": {
"dev": "nodemon src/server.js",
"test": "jest"
}
}
Step 2: Create a minimal Express server
Start with a simple server that has a health check and middleware. This gives you a working base to build on.
src/server.js:
require("dotenv").config();
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const rateLimit = require("express-rate-limit");
const app = express();
app.use(cors());
app.use(helmet());
app.use(express.json());
app.use(morgan("dev"));
app.use(
rateLimit({
windowMs: 15 * 60 * 1000,
max: 200
})
);
app.get("/health", (req, res) => {
res.json({ ok: true, status: "healthy" });
});
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`API running on :${PORT}`));
Run the server locally:
npm run dev
Open http://localhost:4000/health to confirm it works. This small win keeps momentum high early on.
Step 3: Add PostgreSQL + Prisma (database + migrations)
A database-backed project is far more credible than in-memory storage. It proves you can model real data and persist it properly.
Initialize Prisma:
npx prisma init
In your .env, add your database connection string. Example format:
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/task_manager"
JWT_SECRET="replace-with-a-long-random-secret"
JWT_EXPIRES_IN="7d"
Now define your schema in prisma/schema.prisma. This is where you model users and tasks.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String
createdAt DateTime @default(now())
tasks Task[]
}
model Task {
id String @id @default(cuid())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id])
@@index([userId])
}
Run your first migration to create tables:
npx prisma migrate dev --name init
Create a Prisma helper so you can reuse one client instance everywhere.
src/utils/prisma.js:
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
module.exports = prisma;
This setup is already real world. You've got schema, migrations, and a clean DB layer.
Step 4: Build authentication (register + login)
Auth is one of the biggest hiring signals. If you can implement it safely, you're past the beginner-only stage.
You'll build two endpoints: register and login. Users will get a JWT token they can use to access protected routes.
Add validation schemas
Input validation protects your API and keeps your logic clean. It also shows professional habits.
src/schemas/authSchemas.js:
const { z } = require("zod");
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
module.exports = { registerSchema, loginSchema };
Create the auth controller
This controller hashes passwords on signup and compares hashes on login. It then returns a signed JWT.
src/controllers/authController.js:
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const prisma = require("../utils/prisma");
const { registerSchema, loginSchema } = require("../schemas/authSchemas");
function signToken(user) {
return jwt.sign(
{ sub: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || "7d" }
);
}
async function register(req, res) {
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const { email, password } = parsed.data;
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) return res.status(409).json({ error: "Email already in use." });
const hashed = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: { email, password: hashed }
});
const token = signToken(user);
res.status(201).json({ token });
}
async function login(req, res) {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const { email, password } = parsed.data;
const user = await prisma.user.findUnique({ where: { email } });
if (!user) return res.status(401).json({ error: "Invalid credentials." });
const ok = await bcrypt.compare(password, user.password);
if (!ok) return res.status(401).json({ error: "Invalid credentials." });
const token = signToken(user);
res.json({ token });
}
module.exports = { register, login };
Add auth routes
Keep routes thin and push logic into controllers. This makes the project easy to maintain and review.
src/routes/authRoutes.js:
const router = require("express").Router();
const { register, login } = require("../controllers/authController");
router.post("/register", register);
router.post("/login", login);
module.exports = router;
Mount these routes in src/server.js:
const authRoutes = require("./routes/authRoutes");
app.use("/api/auth", authRoutes);
You now have:
- POST /api/auth/register
- POST /api/auth/login
Test these endpoints with Postman, Insomnia, or curl. Save example requests for your README later.
Step 5: Protect routes with JWT middleware

A token is only useful if you enforce it. Middleware is the cleanest way to protect endpoints across your application.
Create an authRequired middleware that checks for a Bearer token and verifies it.
src/middlewares/auth.js:
const jwt = require("jsonwebtoken");
function authRequired(req, res, next) {
const header = req.headers.authorization || "";
const [type, token] = header.split(" ");
if (type !== "Bearer" || !token) {
return res.status(401).json({ error: "Missing or invalid Authorization header." });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = { id: payload.sub, email: payload.email };
next();
} catch {
res.status(401).json({ error: "Invalid or expired token." });
}
}
module.exports = { authRequired };
This middleware also attaches req.user. That makes it easy to enforce ownership, which is crucial for multi-user APIs.
Step 6: Build CRUD endpoints for tasks
CRUD endpoints prove you can create real backend features. The key is doing it in a way that respects authentication and data ownership.
You'll build a list, create, update, and delete endpoints. You'll also add pagination and optional filtering to make it feel real.
Add task validation schemas
Validation prevents bad data from entering your database. It also makes your API behavior consistent.
src/schemas/taskSchemas.js:
const { z } = require("zod");
const createTaskSchema = z.object({
title: z.string().min(1).max(120)
});
const updateTaskSchema = z.object({
title: z.string().min(1).max(120).optional(),
completed: z.boolean().optional()
});
module.exports = { createTaskSchema, updateTaskSchema };
Create the task controller
Notice how every query includes userId: req.user.id. That prevents users from accessing each other's data.
src/controllers/taskController.js:
const prisma = require("../utils/prisma");
const { createTaskSchema, updateTaskSchema } = require("../schemas/taskSchemas");
async function listTasks(req, res) {
const { completed, page = "1", limit = "10" } = req.query;
const pageNum = Math.max(parseInt(page, 10), 1);
const limitNum = Math.min(Math.max(parseInt(limit, 10), 1), 50);
const where = {
userId: req.user.id,
...(completed !== undefined ? { completed: completed === "true" } : {})
};
const [items, total] = await Promise.all([
prisma.task.findMany({
where,
orderBy: { createdAt: "desc" },
skip: (pageNum - 1) * limitNum,
take: limitNum
}),
prisma.task.count({ where })
]);
res.json({
items,
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum)
});
}
async function createTask(req, res) {
const parsed = createTaskSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const task = await prisma.task.create({
data: { title: parsed.data.title, userId: req.user.id }
});
res.status(201).json(task);
}
async function updateTask(req, res) {
const parsed = updateTaskSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const id = req.params.id;
const existing = await prisma.task.findFirst({
where: { id, userId: req.user.id }
});
if (!existing) return res.status(404).json({ error: "Task not found." });
const updated = await prisma.task.update({
where: { id },
data: parsed.data
});
res.json(updated);
}
async function deleteTask(req, res) {
const id = req.params.id;
const existing = await prisma.task.findFirst({
where: { id, userId: req. user.id }
});
if (!existing) return res.status(404).json({ error: "Task not found." });
await prisma.task.delete({ where: { id } });
res.status(204).send();
}
module.exports = { listTasks, createTask, updateTask, deleteTask };
Create the routes
Use router.use(authRequired) so every task endpoint is protected automatically.
src/routes/taskRoutes.js:
const router = require("express").Router();
const { authRequired } = require("../middlewares/auth");
const { listTasks, createTask, updateTask, deleteTask } = require("../controllers/taskController");
router.use(authRequired);
router.get("/", listTasks);
router.post("/", createTask);
router.patch("/:id", updateTask);
router.delete("/:id", deleteTask);
module.exports = router;
Mount it in src/server.js:
const taskRoutes = require("./routes/taskRoutes");
app.use("/api/tasks", taskRoutes);
At this point, you've built the backbone of a professional backend. You can authenticate users and securely store real data.
Make your API clean: status codes, errors, and consistency
Professional APIs are predictable. They return consistent shapes and error patterns across endpoints.
Use sensible status codes:
- 201 for created resources
- 200 for successful reads/updates
- 204 for successful deletions with no body
- 400 for validation problems
- 401 for missing/invalid auth
- 404 for missing resources
Add a basic error handler at the end of server.js. This prevents crashing and avoids leaking internal errors to users.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "Something went wrong." });
});
As your project grows, you can improve this with custom error classes. For now, consistency is what matters.
Security checklist (small upgrades that look impressive)
Security can feel scary, but beginner-friendly steps go a long way. These are the details that make reviewers trust your project.
- Hash passwords with bcrypt (never store plaintext)
- Keep secrets in
.env(never commit them) - Add rate limiting (especially for login/register)
- Validate inputs (Zod/Joi)
- Enforce ownership checks (users can only access their data)
- Use Helmet for safer headers
- Return safe error messages in production
If you implement these, your project feels like it was built by someone ready to work on a team.
Add documentation (so anyone can test your work)
Documentation makes your project easy to review quickly. That matters because most hiring screens are fast.
At a minimum, your README should include:
- Base URL (local + deployed)
- How auth works (Bearer token)
- Endpoint list + examples
- Setup steps + environment variables
- How to run migrations and tests
If you want extra credibility, add Swagger/OpenAPI. Even a small OpenAPI spec can make your API feel more professional.
Add a few meaningful tests (no need to overdo it)
Testing shows you can verify behavior and avoid regressions. For junior roles, 4 to 6 strong tests are plenty.
Test the most important flows:
- Register returns 201
- Login returns a token
- Protected route returns 401 without a token
- Create a task that works with a token
- List tasks returns only the user's tasks
You can write these with Jest + Supertest. Keep them simple and readable. Clarity beats cleverness.
Deploy your API (this is a portfolio multiplier)
A deployed project is easier to trust. It also saves reviewers time, because they can test without cloning. Choose a platform like Render, Railway, or Fly.io. Pair it with a hosted PostgreSQL database to reduce setup friction. When you deploy, set environment variables carefully. Run migrations as part of your deploy process so your database matches your schema.
Portfolio strategy: how to present this project like a pro

A strong project can still be ignored if it's hard to understand. Your job is to make the review effortless.
Write a README that reads like a product page. Add clear steps, example requests, and a short feature list.
Include a "What I learned" section with 4 to 6 bullets. This helps interviewers see your thinking, not just your code.
If you can, add a 2 to 3-minute demo video. Show register/login, create tasks, and pagination in action.
First job strategy: turn this API into interviews
A career change succeeds when your work matches how hiring works. Your project is a tool, so use it deliberately.
Use a results-based CV line
Instead of "learning Node," write something like:
- "Built and deployed a Node.js REST API with JWT auth and PostgreSQL."
- "Implemented validation, pagination, and endpoint tests with Jest/Supertest."
These lines sound like real work because they describe real deliverables. That's exactly what hiring teams want.
Apply for roles that match your proof
This project supports applications for:
- Junior backend developer (Node.js)
- Junior full-stack developer (Node + React)
- Backend internship/apprenticeship
- Support/implementation engineer (often a great bridge role)
You don't need to match every requirement. You need proof you can build, learn, and ship.
Prepare 5 interview stories from this project
Be ready to explain, in plain language:
- How JWT authentication works
- Why hashing is required for passwords
- How you modeled User-Task relationships
- How did you enforce data ownership
- What you'd improve next (refresh tokens, roles, Docker, caching)
If you can explain these clearly, you'll sound like a developer, not a student.
How Code Labs Academy fits into this journey
If you want a structured path, feedback, and accountability, an online bootcamp can help you move faster with fewer dead ends.
Code Labs Academy offers live, remote programs for career changers and upskillers across multiple tracks. You can explore all options on the Courses page, then compare a few common tracks:
Code Labs Academy programs are designed to build job-ready skills through hands-on projects. That includes the same pillars you used here: APIs, authentication, databases, testing, and deployment.
You can also learn more about Career Services if you want support with CV/LinkedIn feedback, interview prep, and job-search strategy. If you'd like to talk it through, you can schedule a call to ask questions and confirm fit.
Common mistakes to avoid (so you keep momentum)
Don't try to build everything at once. Follow a clear sequence: setup, database, auth, CRUD, validation, docs, tests, deploy.
Avoid copy-paste coding without understanding. You should be able to explain every file and decision in simple terms.
Don't skip deployment and documentation. A live link and a clear README can do more for your job search than another half-finished repo.
Conclusion: your next best step
You don't need endless tutorials to break into tech. You need a few projects that clearly prove you can build real features.
Start by shipping this: a Node.js REST API with JWT authentication and a database. Document it, deploy it, and use it as your proof of skill.
When you're ready to accelerate with structured learning and career support, explore Code Labs Academy courses and apply. If you have questions first, you can also contact us or book a call.