CodingNic

Capstone: A Full CRUD API with a Real Database

The Complete API

Capstone: A Full CRUD API with a Real Database 20 min read

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

text
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

text
// prisma/schema.prisma
model Task {
  id    Int     @id @default(autoincrement())
  title String
  done  Boolean @default(false)
}
bash
npx prisma migrate dev --name init

The Model

The Prisma-backed models/taskModel.js from the last lesson, unchanged here.

The Controller

javascript
// 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

javascript
// 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;
javascript
// 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

bash
curl -X POST http://localhost:4700/api/v1/tasks -H "Content-Type: application/json" -d '{"title":"Write capstone lesson"}'
text
{"id":1,"title":"Write capstone lesson","done":false}
bash
curl http://localhost:4700/api/v1/tasks
text
[{"id":1,"title":"Write capstone lesson","done":false}]
bash
curl -X PUT http://localhost:4700/api/v1/tasks/1 -H "Content-Type: application/json" -d '{"done":true}'
text
{"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:

bash
curl http://localhost:4700/api/v1/tasks
text
[{"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

  1. Build this API against your own PostgreSQL database, following the schema, model, controller, and routes above.
  2. Confirm every endpoint (GET, POST, PUT, DELETE) behaves exactly as shown.
  3. Restart the server, and confirm previously created tasks are still there.
  4. Add a GET /api/v1/tasks?done=true filter (Node.js & Express Foundations, Module 9 covered query-parameter filtering) to the index controller function and the Prisma model’s getAll, using where: { 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.