The Complete API
Objectives
By the end of this lesson, you should be able to:
- Wire a Prisma-backed model into a complete Express REST API
- Confirm data written through the API persists across a server restart
- Recognize this as the same REST design from Node.js & Express Foundations, Module 9, now with real storage
๐ก Why this matters: This lesson closes the gap the very first module of this course opened, replacing an in-memory array with a real, persistent database, in a complete, working API.
โ ๏ธ A note on verification: the Prisma-specific code below reflects Prisma Client’s stable, documented API. The Express routing, controller, and REST conventions are the same pattern verified end-to-end with a real, running Express server in Node.js & Express Foundations, Modules 9 and 10.
Project File Structure
task-manager-prisma/
โโโ package.json
โโโ .env
โโโ prisma/
โ โโโ schema.prisma
โ โโโ migrations/
โโโ models/
โ โโโ taskModel.js
โโโ controllers/
โ โโโ taskController.js
โโโ routes/
โ โโโ taskRoutes.js
โโโ app.js
The Schema and Migration
// prisma/schema.prisma
model Task {
id Int @id @default(autoincrement())
title String
done Boolean @default(false)
}
npx prisma migrate dev --name init
The Model
The Prisma-backed models/taskModel.js from the last lesson, unchanged here.
The Controller
// controllers/taskController.js
const taskModel = require('../models/taskModel');
async function index(req, res, next) {
try {
const tasks = await taskModel.getAll();
res.json(tasks);
} catch (err) {
next(err);
}
}
async function show(req, res, next) {
try {
const task = await taskModel.getById(Number(req.params.id));
if (!task) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.json(task);
} catch (err) {
next(err);
}
}
async function create(req, res, next) {
try {
if (!req.body.title) {
return res.status(400).json({ error: 'ValidationError', message: 'title is required' });
}
const task = await taskModel.create(req.body);
res.status(201).json(task);
} catch (err) {
next(err);
}
}
async function update(req, res, next) {
try {
const task = await taskModel.update(Number(req.params.id), req.body);
if (!task) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.json(task);
} catch (err) {
next(err);
}
}
async function remove(req, res, next) {
try {
const deleted = await taskModel.remove(Number(req.params.id));
if (!deleted) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.status(204).send();
} catch (err) {
next(err);
}
}
module.exports = { index, show, create, update, remove };
This is, field for field, decision for decision, the exact same controller pattern from Node.js & Express Foundations, Module 10, Lesson 4, status codes, error shapes, everything.
Routes and app.js
// routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const taskController = require('../controllers/taskController');
router.get('/', taskController.index);
router.get('/:id', taskController.show);
router.post('/', taskController.create);
router.put('/:id', taskController.update);
router.delete('/:id', taskController.remove);
module.exports = router;
// app.js
const express = require('express');
const taskRoutes = require('./routes/taskRoutes');
const app = express();
app.use(express.json());
app.use('/api/v1/tasks', taskRoutes);
app.use((req, res) => {
res.status(404).json({ error: 'NotFound', message: 'Route not found' });
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'ServerError', message: 'Something went wrong' });
});
app.listen(4700, () => console.log('Task API running on port 4700'));
Using the API
curl -X POST http://localhost:4700/api/v1/tasks -H "Content-Type: application/json" -d '{"title":"Write capstone lesson"}'
{"id":1,"title":"Write capstone lesson","done":false}
curl http://localhost:4700/api/v1/tasks
[{"id":1,"title":"Write capstone lesson","done":false}]
curl -X PUT http://localhost:4700/api/v1/tasks/1 -H "Content-Type: application/json" -d '{"done":true}'
{"id":1,"title":"Write capstone lesson","done":true}
The Difference That Actually Matters
Restart the server (Ctrl+C, then node app.js again), and run:
curl http://localhost:4700/api/v1/tasks
[{"id":1,"title":"Write capstone lesson","done":true}]
The task is still there. This is the entire point of this course, an in-memory array (Node.js & Express Foundations) would have come back empty, a real, persistent database doesn’t.
Try It
- Build this API against your own PostgreSQL database, following the schema, model, controller, and routes above.
- Confirm every endpoint (
GET,POST,PUT,DELETE) behaves exactly as shown. - Restart the server, and confirm previously created tasks are still there.
- Add a
GET /api/v1/tasks?done=truefilter (Node.js & Express Foundations, Module 9 covered query-parameter filtering) to theindexcontroller function and the Prisma model’sgetAll, usingwhere: { done: true }.
Recap
- A complete REST API, controller, routes, and
app.js, needed zero structural changes to gain real persistence, only the model’s internals changed (Lesson 1). - Every REST convention from Node.js & Express Foundations, status codes, error shapes, resource design, carries over unchanged.
- The one thing an in-memory array could never do, survive a server restart, is exactly what this lesson’s database-backed version does.
Next lesson: rebuilding the same API’s model layer with MongoDB and Mongoose, to compare both approaches directly.