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 address | Action | Response code |
|---|---|---|
GET /api/posts | List of posts | 200 |
GET /api/posts/:id | One post | 200 or 404 |
POST /api/posts | Create a post | 201 or 422 |
PATCH /api/posts/:id | Update a post | 200 or 404 |
DELETE /api/posts/:id | Delete a post | 204 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/1The 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.