Building a REST API with Sutando and Express
Building a REST API with Sutando is straightforward. In this tutorial, we'll build a complete blog API with authentication, validation, and error handling.
Building a REST API with Sutando is straightforward. In this tutorial, we’ll build a complete blog API with authentication, validation, and error handling.
Project Setup
mkdir blog-api && cd blog-api
npm init -y
npm install sutando mysql2 express zod
npm install -D typescript @types/node @types/express tsx
Database and Models
// db.ts
import { sutando, Model } from 'sutando';
sutando.addConnection({
client: 'mysql2',
connection: {
host: '127.0.0.1', user: 'root', password: '', database: 'blog'
}
});
class User extends Model {
table = 'users';
hidden = ['password'];
relationPosts() { return this.hasMany(Post, 'user_id'); }
}
class Post extends Model {
table = 'posts';
casts = { published: 'boolean' };
relationUser() { return this.belongsTo(User, 'user_id'); }
relationComments() { return this.hasMany(Comment, 'post_id'); }
scopePublished(query) { return query.where('published', true); }
}
class Comment extends Model {
table = 'comments';
relationUser() { return this.belongsTo(User, 'user_id'); }
}
export { sutando, User, Post, Comment };
Validation with Zod
// validators.ts
import { z } from 'zod';
export const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
user_id: z.number().int().positive(),
});
export const updatePostSchema = z.object({
title: z.string().min(1).max(200).optional(),
content: z.string().min(1).optional(),
published: z.boolean().optional(),
});
export const createCommentSchema = z.object({
content: z.string().min(1),
user_id: z.number().int().positive(),
});
The API
// app.ts
import express from 'express';
import { User, Post, Comment } from './db';
import { createPostSchema, updatePostSchema, createCommentSchema } from './validators';
const app = express();
app.use(express.json());
// Error handler
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
// Posts
app.get('/posts', asyncHandler(async (req, res) => {
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 20;
const posts = await Post.query()
.with('user', 'comments.user')
.published()
.orderBy('created_at', 'desc')
.paginate(page, limit);
res.json(posts);
}));
app.get('/posts/:id', asyncHandler(async (req, res) => {
const post = await Post.query()
.with('user', 'comments.user')
.find(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
res.json(post);
}));
app.post('/posts', asyncHandler(async (req, res) => {
const data = createPostSchema.parse(req.body);
const post = await Post.query().create(data);
res.status(201).json(post);
}));
app.put('/posts/:id', asyncHandler(async (req, res) => {
const post = await Post.query().find(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
const data = updatePostSchema.parse(req.body);
post.fill(data);
await post.save();
res.json(post);
}));
app.delete('/posts/:id', asyncHandler(async (req, res) => {
const post = await Post.query().find(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
await post.delete();
res.json({ success: true });
}));
// Comments
app.post('/posts/:id/comments', asyncHandler(async (req, res) => {
const post = await Post.query().find(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
const data = createCommentSchema.parse({ ...req.body, post_id: post.id });
const comment = await Comment.query().create(data);
res.status(201).json(comment);
}));
// Users
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.query()
.with('posts')
.find(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
}));
// Error middleware
app.use((err, req, res, next) => {
if (err.name === 'ZodError') {
return res.status(400).json({ error: 'Validation error', details: err.errors });
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => console.log('API running on port 3000'));
Key Features Demonstrated
- Pagination via
.paginate(page, limit) - Eager loading with
.with('user', 'comments.user') - Query scopes with
.published() - Validation with Zod schemas
- Error handling with async wrapper and error middleware
- Hidden fields (password) via
hiddenarray on model
Next Steps
- Add JWT authentication middleware
- Add rate limiting with express-rate-limit
- Add CORS configuration
- Deploy to your preferred platform
Full documentation at sutando.org.