Skip to main content
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

1
Create the Endpoint Directory
2
Create a directory for your endpoint:
3
mkdir -p extensions/my-extension/src/api/createFoo
4
The directory name determines the route path. For example, createFoo would be accessible at /foos.
5
Create route.json
6
Define the route configuration:
7
{
  "methods": ["POST"],
  "path": "/foos",
  "access": "private"
}
8
Access Levels:
  • public - Accessible to everyone
  • private - Requires authentication
9
Create Middleware
10
Create a body parser middleware:
11
import bodyParser from 'body-parser';

export default (request, response, next) => {
  bodyParser.json({ inflate: false })(request, response, next);
};
12
Create the Handler
13
Create the main handler with middleware ordering:
14
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
    }
  });
};
15
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:
  1. authenticate.ts
  2. validateInput.ts
  3. 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

  1. Create a new request
  2. Set the method (GET, POST, PUT, DELETE)
  3. Enter the URL: http://localhost:3000/foos
  4. Add headers: Content-Type: application/json
  5. Add body (for POST/PUT): {"name":"Foo","description":"Description"}
  6. 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

Common Response Formats

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)

  1. Verify route.json exists and is valid
  2. Check the path matches your request URL
  3. Run npm run build to rebuild
  4. Check extension is enabled in config/default.json

Middleware Not Executing

  1. Verify middleware filename uses bracket notation
  2. Check middleware exports a function
  3. Ensure middleware calls next() when done

Body Parser Not Working

  1. Verify body parser middleware is first in chain
  2. Check Content-Type header is application/json
  3. Ensure body parser is imported correctly

Next Steps

Event Subscribers

React to API events with subscribers

GraphQL Types

Learn about GraphQL API