Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A REST API for posts

An API is a set of server addresses that answer with JSON data instead of HTML pages. Mobile apps, React or Vue frontends and other services use them. REST is an agreement on how to organise such addresses: a collection of records has one address, and the action is chosen by the HTTP method.

Conventions

Method and addressActionResponse code
GET /api/postsList of posts200
GET /api/posts/:idOne post200 or 404
POST /api/postsCreate a post201 or 422
PATCH /api/posts/:idUpdate a post200 or 404
DELETE /api/posts/:idDelete a post204 or 404

The model

Add two functions to src/models/post.js from the database lesson:

// src/models/post.js
export function update(id, { title, body }) {
  db.prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?').run(title, body, id);
}
 
export function remove(id) {
  return db.prepare('DELETE FROM posts WHERE id = ?').run(id).changes > 0;
}

changes is how many rows the query affected. If it is zero, there was no post with that id.

The controller

The API is kept apart from the pages. Create src/controllers/api/postsController.js:

// src/controllers/api/postsController.js
import * as Post from '../../models/post.js';
import { postSchema } from '../../validation/postSchema.js';
 
export function index(req, res) {
  res.json(Post.findAll());
}
 
export function show(req, res) {
  const post = Post.findById(Number(req.params.id));
  if (!post) return res.status(404).json({ error: 'Post not found' });
  res.json(post);
}
 
export function store(req, res) {
  const result = postSchema.safeParse(req.body ?? {});
  if (!result.success) {
    return res.status(422).json({ errors: result.error.issues.map((issue) => issue.message) });
  }
  const id = Post.create(result.data);
  res.status(201).json(Post.findById(id));
}
 
export function update(req, res) {
  const post = Post.findById(Number(req.params.id));
  if (!post) return res.status(404).json({ error: 'Post not found' });
 
  const result = postSchema.partial().safeParse(req.body ?? {});
  if (!result.success) {
    return res.status(422).json({ errors: result.error.issues.map((issue) => issue.message) });
  }
  Post.update(post.id, { ...post, ...result.data });
  res.json(Post.findById(post.id));
}
 
export function destroy(req, res) {
  if (!Post.remove(Number(req.params.id))) {
    return res.status(404).json({ error: 'Post not found' });
  }
  res.sendStatus(204);
}

postSchema is the same schema that validates the form. partial() makes every field optional: an update may send only the title.

Routes

// src/routes/api/posts.js
import { Router } from 'express';
import * as api from '../../controllers/api/postsController.js';
 
const router = Router();
 
router.get('/', api.index);
router.post('/', api.store);
router.get('/:id', api.show);
router.patch('/:id', api.update);
router.delete('/:id', api.destroy);
 
export default router;

In src/app.js mount it after express.json(): app.use('/api/posts', apiPostsRouter).

Trying it out

curl http://localhost:3000/api/posts
curl -X POST http://localhost:3000/api/posts -H "Content-Type: application/json" -d '{"title":"A post from the API","body":"Text"}'
curl -X PATCH http://localhost:3000/api/posts/1 -H "Content-Type: application/json" -d '{"title":"New title"}'
curl -X DELETE http://localhost:3000/api/posts/1

The first command returns the array of posts, the second the new post with its id, the third the updated one, the fourth an empty response with code 204.

Common mistake

API errors must be JSON too. If the 404 handler and the error handler return an HTML page, the client program cannot parse it. Add separate handlers for /api addresses that answer with res.json() — for example, app.use('/api', (req, res) => res.status(404).json({ error: 'Not found' })) before the general 404 page.

Thanks for reading!

Donate to the site — we work hard for you.