API endpoints allow you to create custom RESTful routes for your EverShop store. They’re perfect for handling form submissions, webhooks, integrations, and custom business logic.
Overview
EverShop uses a file-based routing system where:
Directory name = endpoint path
route.json = route configuration (methods, access)
Middleware files = request handlers with execution order
Directory Structure
API endpoints are created in the api folder of your extension:
extensions/[extension-name]/
└── src/
└── api/
└── [endpoint-name]/
├── route.json # Route configuration
├── [middleware1]handler.ts # Middleware with order
└── middleware1.ts # Middleware function
Creating Your First API Endpoint
Create the Endpoint Directory
Create a directory for your endpoint:
mkdir -p extensions/my-extension/src/api/createFoo
The directory name determines the route path. For example, createFoo would be accessible at /foos.
Define the route configuration:
{
"methods" : [ "POST" ],
"path" : "/foos" ,
"access" : "private"
}
Access Levels:
public - Accessible to everyone
private - Requires authentication
Create a body parser middleware:
import bodyParser from 'body-parser' ;
export default ( request , response , next ) => {
bodyParser . json ({ inflate: false })( request , response , next );
};
Create the main handler with middleware ordering:
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default ( request : EvershopRequest , response : EvershopResponse , next ) => {
const { name , description } = request . body ;
if ( ! name || ! description ) {
return response
. status ( 400 )
. json ({ error: 'Name and description are required' });
}
// Create the new foo item
const newFoo = {
id: Date . now (),
name ,
description
};
// Simulate saving to a database
console . log ( 'Creating new foo:' , newFoo );
// Respond with the created foo item
response . status ( 201 ). json ({
success: true ,
data: {
foo: newFoo
}
});
};
The [bodyParser] prefix ensures the body parser runs before the handler. This is the middleware execution order.
Middleware Execution Order
Middleware execution is controlled by filename prefixes in square brackets:
[middleware1]handler.ts # Runs after middleware1
[middleware1][middleware2]handler.ts # Runs after both
Example Middleware Chain
src/api/createProduct/
├── route.json
├── authenticate.ts
├── [authenticate]validateInput.ts
└── [authenticate][validateInput]createProduct.ts
Execution order:
authenticate.ts
validateInput.ts
createProduct.ts
Route Configuration
HTTP Methods
Specify which HTTP methods are allowed:
{
"methods" : [ "GET" , "POST" , "PUT" , "DELETE" ],
"path" : "/api/items" ,
"access" : "public"
}
Custom Paths
Override the default path:
{
"methods" : [ "POST" ],
"path" : "/api/v2/custom/endpoint" ,
"access" : "private"
}
Dynamic Parameters
Use URL parameters:
{
"methods" : [ "GET" ],
"path" : "/api/items/:id" ,
"access" : "public"
}
Access in handler:
export default ( request , response ) => {
const { id } = request . params ;
// ...
};
Real-World Examples
GET Endpoint
Fetch data from the database:
src/api/getFoos/getFoos.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default async ( request : EvershopRequest , response : EvershopResponse ) => {
try {
// Fetch from database (example)
const foos = [
{ id: 1 , name: 'Foo' , description: 'This is a Foo object' },
{ id: 2 , name: 'Bar' , description: 'This is a Bar object' }
];
response . json ({
success: true ,
data: { foos }
});
} catch ( error ) {
response . status ( 500 ). json ({
success: false ,
error: 'Failed to fetch foos'
});
}
};
src/api/getFoos/route.json
{
"methods" : [ "GET" ],
"path" : "/foos" ,
"access" : "public"
}
POST with Validation
Create a resource with input validation:
src/api/createFoo/validateInput.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default ( request : EvershopRequest , response : EvershopResponse , next ) => {
const { name , description } = request . body ;
const errors = [];
if ( ! name || name . trim (). length === 0 ) {
errors . push ({ field: 'name' , message: 'Name is required' });
}
if ( ! description || description . trim (). length < 10 ) {
errors . push ({
field: 'description' ,
message: 'Description must be at least 10 characters'
});
}
if ( errors . length > 0 ) {
return response . status ( 400 ). json ({
success: false ,
errors
});
}
next ();
};
src/api/createFoo/[bodyParser][validateInput]createFoo.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default async ( request : EvershopRequest , response : EvershopResponse ) => {
try {
const { name , description } = request . body ;
// Save to database (example)
const newFoo = {
id: Date . now (),
name ,
description ,
createdAt: new Date (). toISOString ()
};
response . status ( 201 ). json ({
success: true ,
data: { foo: newFoo }
});
} catch ( error ) {
response . status ( 500 ). json ({
success: false ,
error: 'Failed to create foo'
});
}
};
PUT/PATCH Endpoint
Update a resource:
src/api/updateFoo/[bodyParser]updateFoo.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default async ( request : EvershopRequest , response : EvershopResponse ) => {
try {
const { id } = request . params ;
const { name , description } = request . body ;
// Update in database (example)
const updatedFoo = {
id: parseInt ( id ),
name ,
description ,
updatedAt: new Date (). toISOString ()
};
response . json ({
success: true ,
data: { foo: updatedFoo }
});
} catch ( error ) {
response . status ( 500 ). json ({
success: false ,
error: 'Failed to update foo'
});
}
};
src/api/updateFoo/route.json
{
"methods" : [ "PUT" , "PATCH" ],
"path" : "/foos/:id" ,
"access" : "private"
}
DELETE Endpoint
Delete a resource:
src/api/deleteFoo/deleteFoo.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default async ( request : EvershopRequest , response : EvershopResponse ) => {
try {
const { id } = request . params ;
// Delete from database (example)
console . log ( `Deleting foo with id: ${ id } ` );
response . json ({
success: true ,
message: 'Foo deleted successfully'
});
} catch ( error ) {
response . status ( 500 ). json ({
success: false ,
error: 'Failed to delete foo'
});
}
};
src/api/deleteFoo/route.json
{
"methods" : [ "DELETE" ],
"path" : "/foos/:id" ,
"access" : "private"
}
Authentication & Authorization
Private Endpoints
Endpoints with "access": "private" require authentication:
{
"methods" : [ "POST" ],
"path" : "/api/admin/action" ,
"access" : "private"
}
Custom Authentication
Create an authentication middleware:
src/api/protected/authenticate.ts
import { EvershopRequest , EvershopResponse } from '@evershop/evershop' ;
export default ( request : EvershopRequest , response : EvershopResponse , next ) => {
const token = request . headers . authorization ?. replace ( 'Bearer ' , '' );
if ( ! token ) {
return response . status ( 401 ). json ({
success: false ,
error: 'Authentication required'
});
}
// Verify token (example)
try {
// Validate token logic here
request . user = { id: 1 , email: 'user@example.com' };
next ();
} catch ( error ) {
return response . status ( 401 ). json ({
success: false ,
error: 'Invalid token'
});
}
};
Error Handling
Standard Error Response
try {
// Your logic
} catch ( error ) {
console . error ( 'Error:' , error );
response . status ( 500 ). json ({
success: false ,
error: 'An unexpected error occurred'
});
}
Validation Errors
const errors = [];
if ( ! email || ! email . includes ( '@' )) {
errors . push ({ field: 'email' , message: 'Valid email is required' });
}
if ( errors . length > 0 ) {
return response . status ( 400 ). json ({
success: false ,
errors
});
}
Testing API Endpoints
Using cURL
# GET request
curl http://localhost:3000/foos
# POST request
curl -X POST http://localhost:3000/foos \
-H "Content-Type: application/json" \
-d '{"name":"New Foo","description":"A new foo item"}'
# PUT request
curl -X PUT http://localhost:3000/foos/123 \
-H "Content-Type: application/json" \
-d '{"name":"Updated Foo","description":"Updated description"}'
# DELETE request
curl -X DELETE http://localhost:3000/foos/123
Using Postman
Create a new request
Set the method (GET, POST, PUT, DELETE)
Enter the URL: http://localhost:3000/foos
Add headers: Content-Type: application/json
Add body (for POST/PUT): {"name":"Foo","description":"Description"}
Send the request
Best Practices
Use TypeScript : Always define types for request and response data to catch errors early.
Validate Input - Always validate and sanitize user input
Error Handling - Use try-catch blocks and return meaningful error messages
HTTP Status Codes - Use appropriate status codes (200, 201, 400, 401, 404, 500)
Consistent Response Format - Return consistent JSON structure
Security - Never expose sensitive data or internal errors
Logging - Log errors for debugging but don’t expose details to clients
Success Response
{
"success" : true ,
"data" : {
"item" : { ... }
}
}
Error Response
{
"success" : false ,
"error" : "Error message"
}
Validation Error Response
{
"success" : false ,
"errors" : [
{ "field" : "name" , "message" : "Name is required" },
{ "field" : "email" , "message" : "Invalid email format" }
]
}
Troubleshooting
Endpoint Not Found (404)
Verify route.json exists and is valid
Check the path matches your request URL
Run npm run build to rebuild
Check extension is enabled in config/default.json
Middleware Not Executing
Verify middleware filename uses bracket notation
Check middleware exports a function
Ensure middleware calls next() when done
Body Parser Not Working
Verify body parser middleware is first in chain
Check Content-Type header is application/json
Ensure body parser is imported correctly
Next Steps
Event Subscribers React to API events with subscribers
GraphQL Types Learn about GraphQL API