
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
@appgeist/restful-api
Advanced tools
An opinionated, convention-over-configuration Express-based API restful server featuring yup validation

An opinionated, convention-over-configuration Express-based restful API server featuring yup validation.
You can use @appgeist/restful-api either as an Express middleware:
const express = require("express");
const api = require("@appgeist/restful-api");
const [host, port] = ["0.0.0.0", 3000];
express()
// other middleware
.use(api())
// other middleware
.listen(port, host, err => {
if (err) throw err;
// eslint-disable-next-line no-console
console.log(`Server listening on http://${host}:${port}...`);
});
...or directly:
const api = require("@appgeist/restful-api");
const [host, port] = ["0.0.0.0", 3000];
api().listen(port, host, err => {
if (err) throw err;
// eslint-disable-next-line no-console
console.log(`Server listening on http://${host}:${port}...`);
});
When initializing @appgeist/restful-api you can optionally specify the folder containing the route handler definitions (defaults to ./routes):
const path = require("path");
const api = require("@appgeist/restful-api");
const [host, port] = ["0.0.0.0", 3000];
api(path.join(__dirname, "api-routes")).listen(port, host, err => {
if (err) throw err;
// eslint-disable-next-line no-console
console.log(`Server listening on http://${host}:${port}...`);
});
Name your route handler definition modules get.js, post.js, put.js, patch.js or delete.js and organize them in a meaningful folder structure that will get translated to rest API routes.
For instance:
routes/departments/get.js => GET /departments
routes/departments/post.js => POST /departments
routes/departments/[id]/patch.js => PATCH /departments/3
routes/departments/[id]/delete.js => DELETE /departments/3
routes/departments/[id]/employees/get.js => GET /departments/3/employees
routes/departments/[id]/employees/post.js => POST /departments/3/employees
routes/departments/[id]/employees/[id]/get.js => GET /departments/3/employees/165
routes/departments/[id]/employees/[id]/patch.js => PATCH /departments/3/employees/165
routes/departments/[id]/employees/[id]/delete.js => DELETE /departments/3/employees/165
Each module exports:
beforeRequest - an optional before request handler function;onRequest - a mandatory request handler function;paramsSchema, querySchema, bodySchema - optional schemas to validate the incoming request params, query and body. These can be simple objects (for brevity, in which case they will be converted to yup schemas automatically) or yup schemas (for more complex scenarios, i.e. when you need to specify a .noUnknown() modifier).A function handler accepts an optional object parameter in the form of { params, query, body, req } and must return the data that will be sent back to the client, or a Promise resolving with the data.
For simple cases when you don't care about validation, you can export just the handler, like so: module.exports = () => {/* handle request here... */};.
For nested paths, ancestor-level parameter names are magically renamed with the help of pluralize.singular:
routes/departments/[id]/employees/[id]/get.js -> params: { departmentId, id }
routes/departments/[id]/employees/[id]/projects/[id]/get.js -> params: { departmentId, projectId, id }
When request data validation fails, the client will receive a response with HTTP status code 400/BAD_REQUEST and a JSON body describing the error provided by yup.validate(data, { abortEarly: false }):
{
"message": "There were 2 validation errors",
"errors": ["body.name is required", "body.description is required"]
}
If a function handler throws an ApiError(httpStatusCode) (also exported from this package), the client will receive a response with the specified HTTP status code and a JSON body like { "message": "Why it failed" }.
The ApiError constructor accepts an HTTP status code number or an object structured like { status, message }.
Using the constructor without parameters will result in a respose with HTTP status code 500 and the following JSON body:
{
"message": "Internal Server Error"
}
You can override the default error handling mechanism by providing a custom error handling function like so:
const path = require("path");
const api = require("@appgeist/restful-api");
const [host, port] = ["0.0.0.0", 3000];
api(path.join(__dirname, "api-routes"), {
errorHandler: ({ err, res }) => {
res.status(500).send("Error");
console.log(err.stack);
}
}).listen(port, host, err => {
if (err) throw err;
// eslint-disable-next-line no-console
console.log(`Server listening on http://${host}:${port}...`);
});
routes/departments/get.js:
const Department = require("models/Department");
// simple handler, without validation
module.exports = () => Department.listAll();
// ...or exports.onRequest = () => Department.listAll();
routes/departments/[id]/patch.js:
const { object, number } = require('yup');
const { FORBIDDEN } = require('http-status-codes');
const { ApiError } = require('@appgeist/rest-api');
const Department = require('models/Department');
const ACL = require('utils/acl');
const Logger = require('utils/logger');
// schema can be a simple object
exports.paramsSchema = {
id: number().positive().integer().required()
};
// schema can be a yup object
exports.bodySchema = object({
name: string().min(2).max(20).required()
description: string().min(2).max(1000).required()
}).noUnknown();
exports.onRequest = async ({ params: { id }, body, req }) => {
// throwing an ApiError(403) will result in a 403 status code being sent to the client
if (!(ACL.isManager(req.userId))) throw new ApiError(FORBIDDEN);
const department = await Department.updateById(id, { data: body });
await Logger.log(`Department ${id} updated by user ${req.userId}`);
return department;
}
routes/departments/[id]/employees/[id]/get.js:
const { number } = require('yup');
const Department = require('models/Department');
const Employee = require('models/Employee');
exports.paramsSchema = {
departmentId: number().positive().integer().required()
id: number().positive().integer().required()
};
exports.beforeRequest = ({ url }) => {
console.log(`Preparing to respond to ${url}`);
};
exports.onRequest = async ({ params: { departmentId, id } }) => {
const employee = await Employee.getById(id);
employee.department = await Department.getById(departmentId);
return employee;
}
The ISC License.
FAQs
An opinionated, convention-over-configuration Express-based API restful server featuring yup validation
The npm package @appgeist/restful-api receives a total of 27 weekly downloads. As such, @appgeist/restful-api popularity was classified as not popular.
We found that @appgeist/restful-api demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.