Axion Functions is a full-stack development framework designed to simplify the process of developing applications. It leverages JavaScript (JS) and ES Modules, supporting both backend modules and front-end React components, with automatic routing and rendering. Axion Functions runs on the Deno runtime, eliminating the need for npm package management (but still compatible with it).
Get started with Axion Functions in a few simple steps:
-
Install Deno: Download from deno.land
-
Create project structure:
my-project/ ├── api/ │ └── hello.ts ├── pages/ │ └── home.jsx
-
Create a backend module (
api/hello.ts
):export default (props) => { return `Hello, ${props.name || 'World'}!`; };
-
Create a frontend component (
pages/home.jsx
):import React from 'npm:react'; // in Deno, you can use npm: specifiers to import modules without need to `npm install`. const HomePage = (props) => { return <div>Welcome to Axion Functions!</div>; }; export default HomePage;
-
Start the application:
deno run -A https://raw.githubusercontent.com/AxionCompany/functions/main/main.ts
-
Test your application:
- Backend:
curl http://localhost:9002/api/hello?name=Axion
- Frontend: Open
http://localhost:9002/pages/home
in your browser
- Backend:
You now have a basic Axion Functions application up and running!
Our end goal is to increase productivity for JS developers, and our commitment is to always push to the frontier of what web technology allowes, and keeping the utmost simplicity for JS software developers.
Axion Functions was developed by Axion Company, a custom software development company. The main motivation behind creating this framework was to facilitate the development process for our projects, which predominantly use the JavaScript stack. We found ourselves solving similar coding problems across different projects and wanted a way to reuse code snippets efficiently, both for backend modules and frontend components. Beyond reusing front-end components or backend modules that were common across different projects, we've realized other aspects of the lifecycle of a project can be optimized as well, many of which we hope to address with this framework going forward, but always with the core mantra in mind: keep the utmost simplicity for software developers:
- Observability
- Performance
- Deployment in Distributed Systems
The framework draws significant inspiration from several sources:
- Module Federation in Webpack : Developed by the amazing Zach Jackson, Module Federation provides a way to dynamically import modules across different projects. We extended this concept to Deno, taking advantage of its compatibility with Node modules and its ability to import modules from HTTP URLs with security restrictions.
- Deno Deploy : We were amazed by the speed and developer experience of Deno Deploy. The concepts of process isolation and the N+2 port requirement in Axion Functions are (albeit different) inspired by Deno Deploy's architecture, which ensures fast, isolated, and efficient execution of code.
- Next.js : Next.js is a popular React framework that simplifies the development of full-stack applications. We wanted to create a similar experience for Deno developers, allowing them to write backend modules and frontend components in a unified environment with automatic routing and rendering.
We chose Deno as the foundation for Axion Functions because it offers:
- Native TypeScript Support: Deno supports TypeScript out of the box, which aligns with our development needs.
- Secure by Default: Deno's security model allows us to specify permissions for file system access, environment variables, and network requests.
- Node Compatibility: Deno is compatible with Node modules, making it easy to reuse existing JavaScript code.
- HTTP Imports: Deno's ability to import modules directly from URLs simplifies dependency management and enhances modularity.
We believe that developers should spend more time creating and less time configuring. Axion Functions simplifies the development process by providing a unified environment for writing backend and front-end code, with automatic routing and rendering, but still - all of it as isolated processes. With Axion Functions, you can build full-stack applications quickly and efficiently, without the overhead of managing dependencies, build tools, routes, boilerplates, etc. Less configuration, more coding - that's the Axion way.
- Full-Stack Development: Write ES Modules for both backend and front-end components.
- Automatic Routing: File structure determines API endpoint routes and website paths.
- Easy Imports: Directly import npm packages using
npm:
specifiers. - Just-in-Time Building: Automatic, on-the-fly building and caching of files.
- Simplified Setup: No need for
npm install
or manual builds. - Process Isolation: Isolated execution of modules and components to prevent application-wide crashes.
Axion Functions runs on the Deno runtime. Ensure you have Deno installed on your machine. You can download and install Deno from deno.land.
The recommended way of starting the application is by creating a deno.json
file with the following content:
{
"tasks": {
"start": "DENO_DIR=./data/axion/cache/.deno ENV=production deno run -A --importmap=deno.json --no-lock --unstable-sloppy-imports --no-prompt --unstable https://raw.githubusercontent.com/AxionCompany/functions/main/main.ts",
"dev": "DENO_DIR=./data/axion/cache/.deno ENV=development WATCH=true deno run --importmap=deno.json --reload=https://raw.githubusercontent.com/AxionCompany/functions/main -A --no-lock --unstable-sloppy-imports --no-prompt --unstable https://raw.githubusercontent.com/AxionCompany/functions/main/main.ts"
}
}
This configuration defines two tasks:
- start: Runs the application on port 8000.
- dev: Runs the application in development mode with reload-on-save.
// axion.config.json
{
"functionsDir": ".",
"dirEntrypoint": "main"
}
This configuration specifies that the current directory ( .
) should be served as the root directory for modules and components, and the default entry point for directories is main
.
To start the application, run:
deno task start
For development mode, run:
deno task dev
If you prefer to use npm, you can create a package.json file with the following content:
{
"scripts": {
"start": "DENO_DIR=./data/axion/cache/.deno ENV=production deno run -A --importmap=deno.json --no-lock --unstable-sloppy-imports --no-prompt --unstable https://raw.githubusercontent.com/AxionCompany/functions/main/main.ts",
"dev": "DENO_DIR=./data/axion/cache/.deno ENV=development WATCH=true deno run --importmap=deno.json --reload=https://raw.githubusercontent.com/AxionCompany/functions/main -A --no-lock --unstable-sloppy-imports --no-prompt --unstable https://raw.githubusercontent.com/AxionCompany/functions/main/main.ts"
}
}
To start the application using npm, run:
npm run start
For development mode, run:
npm run dev
Both methods will pull Axion Functions' code from GitHub and execute it on your local machine, starting the server.
- p.s.: even if using npm commands, you still need to have Deno installed on your machine. *
- Creating Backend Modules: Use
.ts
or.js
extensions.
Any exported function will be automatically executed for the corresponding http method. If the function is not explicitly defined, the default export function will be executed, which accepts any http method. Any query, body or path parameters will be automatically parsed from the request and passed to the function as parameters.
// api/hello.ts
export default (props) => { // default export functions will be executed for any http method that is not explicitly defined
return `Hello, ${props.name || 'World'}!`;
};
export const GET = (props) => { // GET method will be executed for the api/hello endpoint. If available, takes precedence over the default export function
return `Hello, ${props.name || 'World'} via GET!`;
};
export const POST = (props) => { // POST method will be executed for the api/hello endpoint. If available, takes precedence over the default export function
return `Hello, ${props.name || 'World'} via POST!`;
};
export const PUT = (props) => { // PUT method will be executed for the api/hello endpoint. If available, takes precedence over the default export function
return `Hello, ${props.name || 'World'} via PUT!`;
};
export const DELETE = (props) => { // DELETE method will be executed for the api/hello endpoint. If available, takes precedence over the default export function
return `Hello, ${props.name || 'World'} via DELETE!`;
};
Testing Backend Modules with curl:
# Default export
curl -X GET "http://localhost:9002/api/hello?name=Axion" // will execute the default export function
# GET method
curl -X GET "http://localhost:9002/api/hello/GET?name=Axion"
# POST method
curl -X POST "http://localhost:9002/api/hello/POST" -d '{"name":"Axion"}'
-
Creating Front-End Modules: Use
.jsx
or.tsx
extensions.Axion functions will assume that front-end modules are React components, and will automatically render them in the browser. Any files with the .jsx or .tsx extension will be considered as React components.
// pages/home.jsx import React from 'npm:react'; const HomePage = (props) => { return <div>Welcome, {props.user || 'Guest'}!</div>; }; export default HomePage;
Testing Front-End Modules in Browser:
- Open your browser and navigate to
http://localhost:9002/pages/home?user=Axion
to see the HomePage component in action.
- Open your browser and navigate to
-
Path Parameters: Use
[filename]
syntax for dynamic routes.// pages/[userId]/profile.jsx import React from 'npm:react'; const UserProfile = (props) => { return <div>User Profile for ID: {props.userId}</div>; }; export default UserProfile;
Testing Dynamic Routes in Browser:
- Open your browser and navigate to
http://localhost:9002/pages/123/profile
to see the UserProfile component for user ID 123.
- Open your browser and navigate to
This backend module will handle adding, viewing, and deleting tasks. It will use an in-memory storage for simplicity.
// api/tasks.ts
import { v4 as uuidv4 } from 'npm:uuid';
let tasks = [];
// Get all tasks
export const GET = ({...params}) => { // GET is the default method for the api/tasks endpoint. Any query parameters will be automatically passed to the function
return tasks;
};
// Add a new task
export const POST = ({name}) => { // name is the body parameter, and will be automatically parsed from the request body and passed to the function
const task = { id: uuidv4(), name };
tasks.push(task);
return task;
};
// Delete a task
export const DELETE = ({id}) => { // :id path parameter will automatically be passed to the function
tasks = tasks.filter(task => task.id !== id);
return { success: true };
};
# Get all tasks
curl -X GET "http://localhost:9002/api/tasks?name=Axion"
# Add a new task
curl -X POST "http://localhost:9002/api/tasks" -H "Content-Type: application/json" -d '{"name":"Sample Task"}'
# Delete a task (replace <task-id> with actual task ID)
curl -X DELETE "http://localhost:9002/api/tasks?id=<task-id>"
This front-end component will interact with the backend to display the list of tasks and provide a form to add new tasks.
// pages/tasks.jsx
import React, { useState, useEffect } from 'npm:react';
const TasksPage = ({...props}) => {
const [tasks, setTasks] = useState([]);
const [taskName, setTaskName] = useState('');
useEffect(() => {
fetch('/api/tasks')
.then(response => response.json())
.then(data => setTasks(data));
}, []);
const addTask = (e) => {
e.preventDefault();
fetch('/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: taskName }),
})
.then(response => response.json())
.then(newTask => {
setTasks([...tasks, newTask]);
setTaskName('');
});
};
const deleteTask = (id) => {
fetch(`/api/tasks?id=${id}`, {
method: 'DELETE',
})
.then(() => {
setTasks(tasks.filter(task => task.id !== id));
});
};
return (
<div>
<h1>Task Management</h1>
<form onSubmit={addTask}>
<input
type="text"
value={taskName}
onChange={(e) => setTaskName(e.target.value)}
placeholder="Enter task name"
required
/>
<button type="submit">Add Task</button>
</form>
<ul>
{tasks.map(task => (
<li key={task.id}>
{task.name} <button onClick={() => deleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default TasksPage;
Open your browser and navigate to http://localhost:9002/pages/tasks
to see the task management application in action.
Axion Functions supports "shared" modules, which allow you to reuse code across multiple files in the same directory or any subdirectories. This helps keep your code DRY (Don't Repeat Yourself) and maintainable.
- Create a Shared Module:
// backend/shared.js
import { v4 as uuidv4 } from 'npm:uuid'; // in Deno, we use the npm: prefix to import modules from npm, without the need for npm install
export default (modules) => ({ ...modules, uuid: uuidv4 });
- Consume the Shared Module:
// backend/tasks.ts
export const POST = (body) => {
const { uuid } = POST;
const task = { id: uuid(), ...body };
tasks.push(task);
return task;
};
In this example, the uuid function is shared across the backend directory and its subdirectories, allowing you to easily generate unique IDs in multiple modules without repeating the import statement.
You'll be able to deconstruct it from the exported function name being executed (in this example, from POST
function inside the api/tasks.ts
file).
Axion Functions also supports "layout" components, which allow you to define a common structure or layout for your frontend components. This is particularly useful for elements like headers, footers, and menus that should be consistent across multiple pages.
- Create a Layout Component:
// pages/layout.jsx
export default ({ children }) => {
return (
<>
<header>This is a header</header>
<main>{children}</main>
<footer>This is a footer</footer>
</>
);
};
- Consume the Layout Component:
// pages/home.jsx
import React from 'npm:react';
const HomePage = () => {
return <div>Welcome to the Home Page!</div>;
};
export default HomePage;
In this example, any component declared in the pages directory (or its subdirectories) will automatically be wrapped with the layout component, including the header and footer.
Both "shared" modules and "layout" components can be nested within directories, and they will accumulate from the higher directories. This means you can have multiple layers of shared functionality and layouts, enhancing modularity and code reuse.
Axion Functions allows you to customize the HTML structure of your pages by using index.html files. The closest index.html file to the current path will be considered, while higher-level index.html files will be ignored.
- Create an index.html File:
<!-- pages/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Application</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
In this example, the index.html file will be used as the base HTML structure for all pages within the pages directory and its subdirectories.
Axion Functions supports the use of global CSS files, which can be defined at any level in the folder structure. These CSS files will be accumulated from the root directory up to the directory containing the current file being executed.
- Create a
globals.css
File in root directory:
/* globals.css */
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
- Create a
globals.css
File in apages
directory:
/* pages/globals.css */
h1 {
color: blue;
}
In this example, the styles defined in the globals.css
file at the root directory will be applied to all pages, and the styles in pages/globals.css
will be applied to all pages within the pages directory and its subdirectories, accumulating with the root styles.
Any CSS file named globals.css
will be automatically included in the HTML output, always respecting the hierarchy of the directories.
Axion Functions aims to make the development process as straightforward as possible while still allowing for extensive configuration as applications scale. Configuration can be set via environment variables or a configuration file at the root of the project called axion.config.json.
- FUNCTIONS_DIR : Specifies the root directory to be served as modules or components. Files and directories outside this root will not be available on the web but can still be used in the project. Defaults to
.
(root project directory). - DIR_ENTRYPOINT : Specifies the default file name to be considered as the main entry point in a directory. This makes it unnecessary to specify it when importing by the path of its parent directory. Defaults to index.
- FILE_LOADER_URL: Defines the URL where the file loader is running if the developer wants to run it separately. Defaults to http://localhost:9000.
- FILE_LOADER_PORT: Specifies the port for the file loader. Defaults to 9000.
- DEFAULT_LOADER_TYPE: Specifies the loader type to load the files. Options are local or github, and it defaults to local.
- USE_CACHE: Determines if the cache should be enabled by default when loading a file. Defaults to false if DEFAULT_LOADER_TYPE is local and true otherwise.
- DEBUG: Enables Axion Functions logs for debugging purposes. Defaults to false.
You can also set these configurations in a axion.config.json file at the root of your project. Use camelCase for the properties.
{
"functionsDir": "src",
"dirEntrypoint": "main",
"fileLoaderUrl": "http://localhost:9001",
"fileLoaderPort": 9001,
"defaultLoaderType": "github",
"useCache": true,
"debug": true
}
This configuration provides the flexibility needed for larger applications while maintaining the simplicity and ease of use for smaller projects.
Axion Functions is designed to provide an isolated and robust development environment, preventing errors from affecting the entire application. It achieves this through a system of process isolation and efficient resource management, inspired by Deno Deploy.
Each module or component in Axion Functions runs as an isolated process. This ensures that an error in one endpoint or component does not crash the entire application. This isolation is managed through the Deno runtime, leveraging its capabilities for secure, efficient execution. This approach borrows concepts from Deno Deploy, known for its fast deployment and excellent developer experience.
To run an Axion Functions application, you will need 2+N ports, where N is the number of files that should be executed either as modules or components. The two essential ports are for the file loader and the API server:
- File Loader Port: Used to load and serve files dynamically.
- API Server Port: Handles incoming API requests. Each additional file being executed will run on its own port, managed by the Deno runtime. This structure ensures efficient handling of requests and execution of modules and components.
Axions are the prebuilt modules and components that we have developed for our applications. These axions represent the common functionalities we repeatedly use across different projects. By packaging these reusable pieces of code, we aim to increase productivity and maintain consistency in our applications.
The Axion Functions repository comes with a set of prebuilt axions, both for backend modules and frontend components. These axions are battle-tested in production environments and maintained by our team.
We provide a builtin we for using them in your project by using the adding a reference to this repository directly in your deno.json
importmap.
//deno.json
{
"tasks": {
...tasks
},
"imports": {
"axion-modules/":"https://raw.githubusercontent.com/AxionCompany/functions/main/functions/modules/",
... other imports
}
}
and then, use them in your project like so:
import MongoDbCrud from "axion-modules/features/crud/mongodb";
// use the function as you'd like here...
We intend to provide better documentation and a dedicated website for browsing and using these axions in the future, but for now, please, refer to /functions/components
and /functions/modules
directories in the source code for examples.
Developers can easily extend their applications by incorporating these axions. The modular nature of Axion Functions allows for seamless integration and customization, making it easy to adapt these prebuilt components to specific project requirements.
Axion functions is still in pre-release phase, and we are actively working on improving the framework and adding new features.
Our roadmap for Axion Functions includes the following features and improvements:
- Improved Documentation: Enhance the documentation with more examples, tutorials, and guides.
- Performance Optimization: Optimize the framework for faster execution and better resource management.
- Testing and Quality Assurance: Implement automated testing and quality assurance processes to ensure the stability and reliability of the framework.
- Community Contributions: Encourage community contributions and feedback to improve the framework and make it more accessible to developers.
Axion Functions is still in early phases of development, and we started by prioritizing Developer Experience over performance. We are, however, committed to improving the performance of the framework as we continue to develop it. Our goal is to provide a fast and efficient development environment that can scale with your applications, and we will be working on optimizing the framework in the future.
To do: Add performance benchmarks and optimizations.
We welcome contributions! If you have ideas for new features, improvements, or bug fixes, please open an issue or submit a pull request. We are actively working on improving the framework and would love to have your input.
This project is licensed under the MIT License. See the LICENSE file for details.
For questions or issues, please open an issue in this repository or contact us at [[email protected]].