Module serve
The serve
module is a basic server module used to create and control an HTTP server. It leverages IttyRouter for routing.
Overview
The serve
module simplifies the process of creating an HTTP server by providing a streamlined interface for handling requests, managing routes, and serving files. It includes methods for starting the server, setting the port, and logging messages.
Usage
Here's a quick example of setting up a basic server using the serve
module:
Svr = imp 'serve'
Svr::create fetch: () -> Svr::text "Hello, World!"
.port 3000
.listen
Explanation
imp 'serve'
: Imports theModuleServe
class.Svr::create
: Creates a new server instance.fetch: () -> Svr::text "Hello, World!"
: Defines afetch
function to handle incoming requests and return a plain text response..port 3000
: Sets the server to listen on port 3000..listen
: Starts the server and begins listening for requests.
API Reference
Svr::create(options)
Creates and configures a new server instance.
Parameters:
options
: An object with the following optional properties:handler
: A function(req, res) ->
to handle requests directly. Overridesfetch
if both are provided.routers
: An array of routers to be used by the server.fetch
: A function(req) -> ResponseType
or(req) -> Promise<ResponseType>
to fetch responses for requests.
Returns:
- A
ModuleServeServer
instance with methods to set up and control the server.
Example:
server = Svr::create
fetch: (req) -> Svr::text "Hello from Fetch"
server.port 8080
.listen
.log "Server running on port $port"
Svr::createFileRouter(options)
Creates a file router to serve files from a directory based on request paths.
Parameters:
options
: An object with the following properties:root
: The root directory for file serving.basePath
: (Optional) The base path for routing.resolveExtensions
: (Optional) An array of file extensions to resolve.bundlerOptions
: (Optional) Options for the bundler.bundlerEntry
: (Optional) Custom function for bundler entry.ssrBundlerEntry
: (Optional) Custom function for SSR bundler entry.onError
: (Optional) Function to handle errors.
Returns:
- A function
(req) -> ResponseType | Promise<ResponseType>
for handling file requests.
Example:
fileRouter = Svr::createFileRouter
root: './app'
server = Svr::create fetch: fileRouter
server
.port 3000
.listen
Usage with routers
router = Svr::router
fileRouter = Svr::createFileRouter
root: './app'
basePath: '/app'
router.all '/app', fileRouter
server = Svr::create fetch: router.fetch
server
.port 3000
.listen
Renderable Files
File routers defined in the serve
module are designed to handle different types of files and scenarios based on the naming conventions and their content.
1. path/route.[coffee|js]
If a file named route.coffee
or route.js
exists in the specified path (path
), the file router will import it. This file is expected to contain route handlers or middleware logic that defines how incoming requests to specific routes should be processed and responded to.
- coffee
export GET = -> Svr::text 'This will be returned on get`
2. path/page.[coffee|js|jsx]
When a file named page.coffee
, page.js
, or page.jsx
exists in the specified path (path
), the file router treats it as a page that needs to be rendered into HTML. Here's how it's processed:
Bundle and Render: The file, along with any layout files associated with it, is bundled using the
web
module's bundling capabilities (typically viabundle()
method). This step prepares the script that will be executed on the client side to render the page dynamically.Usage of
render()
Function: Therender()
function is called during this process. It's responsible for generating the HTML content of the page. Here's a typical pattern of howrender()
might be used:coffeeexport render -> div = document.createElement 'div' div.innerText = 'Hello' document.body.appendChild div div
3. path/page.s.[coffee|js|jsx]
If a file named page.s.coffee
, page.s.js
, or page.s.jsx
exists, it indicates that Server-Side Rendering (SSR) is enabled for this page. Here's how it's processed:
SSR Bundling and Rendering: Similar to the previous scenario, but with a focus on server-side rendering. This involves:
- Calling
render()
function within the SSR context to generate the HTML content server-side.
Here's a conceptual example of how SSR and File Router might be used:
coffee# @jsx Web.prototype.createElement Web = imp 'web' export render -> <div>Hello</div>
- Calling
render()
Function Call
The render()
function plays a crucial role in both client-side and server-side rendering scenarios:
Client-Side Rendering: When bundling
page.coffee
,page.js
, orpage.jsx
,render()
is called to generate HTML content that will be dynamically inserted into the DOM on the client side.Server-Side Rendering: When handling
page.s.coffee
,page.s.js
, orpage.s.jsx
,render()
is used within the SSR context. It generates the HTML content on the server side, which is then sent as a fully rendered page to the client.
Props Structure
The props
structure is utilized differently based on whether it's used for CSR, SSR, or within route.coffee
. Here's a detailed explanation of each key within the props
structure:
Common Keys
url
: Represents the current URL of the page being rendered.query
: Contains the current page to be rendered(only on SSR).page
: Contains the query parameters parsed from the URL.method
: Specifies the HTTP method used in the request (e.g., GET, POST).params
: Holds route parameters extracted from the URL path. Parameters can be defined using dynamic segments (e.g.,/home/:id
) or folder-based routing (e.g.,root/[slug]
where[slug]
is a parameter).
Key Specific to Routes (route.coffee
)
page
: Refers to the page or component that is intended to be rendered. This allows routes to determine which page component to render based on the requested URL and parameters.
Usage in Different Contexts
Client-Side Rendering (CSR)
In client-side rendering, the render()
function is typically exported from page.coffee
, page.js
, or page.jsx
. It accepts props
as its sole parameter since it operates without direct access to the server request object. Here’s how it looks in CoffeeScript:
# Example in page.coffee or page.js or page.jsx
export render = (props) ->
console.log(props.url) # Outputs current URL
console.log(props.query) # Outputs query parameters
console.log(props.method) # Outputs HTTP method (e.g., GET)
console.log(props.params) # Outputs route parameters
- When on Layout:coffee
# Example in layout.coffee or layout.js or layout.jsx export render = (props, previousRender) -> console.log props, previousRender
Server-Side Rendering (SSR)
For server-side rendering, the render()
function is exported from page.s.coffee
, page.s.js
, or page.s.jsx
. It accepts both the request
object and props
. This allows SSR to access server-specific details along with the rendering parameters. Here’s an example in CoffeeScript:
# Example in page.s.coffee or page.s.js or page.s.jsx
export render = (req, props) ->
print(req.headers) # Access server request headers
print(props.url) # Outputs current URL
print(props.query) # Outputs query parameters
print(props.method) # Outputs HTTP method (e.g., GET)
print(props.params) # Outputs route parameters
props.page.add <p>Example</p> # Adds to the currently rending page
- When on Layout:coffee
# Example in layout.s.coffee or layout.s.js or layout.s.jsx export render = (props, previousRender) -> props.page.style href: "https://example.com/style.css" # Adds style to the currently rending page props.page.script src: "https://example.com/script.js" # Adds script to the currently rending page <div>{previousRender}</div>
Routes (route.coffee
)
Routes defined in route.coffee
handle incoming requests and direct them to appropriate pages or components based on the URL and route parameters. They also receive props
, which exclude the page
attribute. Here’s how it might look:
# Example in route.coffee
export GET = (req, props) ->
print(props.url) # Outputs current URL
print(props.query) # Outputs query parameters
print(props.method) # Outputs HTTP method (e.g., GET)
print(props.params) # Outputs route parameters
SSR Static Rendering
Static rendering, which means rendering to a static HTML string, can be useful incase you want to make your page completly static when rendered. To do that you can just export this form your page.s.coffee
:
export staticRendering = false
Layout Files
Layout files named layout.s.coffee
, layout.s.js
, or layout.s.jsx
play a crucial role in the serve
module, especially when handling Server-Side Rendering (SSR). Here's a detailed explanation of their purpose and how they interact within the application architecture:
Purpose of Layout Files
Layout files are used to define the structure and common elements that are shared across multiple pages or components within a web application. In the context of the serve
module:
- Server-Side Rendering (SSR): Layout files (
layout.s.*
) are employed to encapsulate common HTML structure, such as headers, footers, navigation bars, etc., that should be consistently rendered across different pages.
Exporting render
Function
Each layout file (layout.s.*
) is expected to export a render
function. This function is essential because:
Composition: It allows the composition of multiple layout files. Each layout can modify or enhance the content passed to it before passing it to the next layout in the stack.
Consistency: By exporting a
render
function, layout files adhere to a standardized interface that ensures they can be seamlessly integrated into the SSR rendering pipeline.
Rendering Stack
When handling a page that requires SSR and utilizes layout files:
Finding Layout Files: The application identifies and collects relevant layout files (
layout.s.*
) based on their presence in the directory structure relative to the main page file (page.s.*
).Rendering Order: Layout files are rendered in reverse order, starting from the outermost layout to the innermost. This approach allows each layout to wrap its content with additional markup or functionality.
Data Propagation: The
render
function of each layout receives data and content from the preceding layout or the main page file. It processes this data, potentially modifying or enhancing it, and then passes it down to the next layout in the stack.Final Output: The final HTML content, after being processed by all relevant layout files, represents the fully rendered page content. This content is then sent as a response to the client's request.
Example Scenario
Consider a scenario where a web application has the following files:
page.s.coffee
: Represents the main page that requires SSR../layout.s.coffee
: An outer layout file providing a common header and footer structure.../layout.s.coffee
: A nested layout file providing additional navigation elements.
Here's how the rendering stack might look:
page.s.coffee
imports./layout.s.coffee
../layout.s.coffee
imports../layout.s.coffee
.
The sequence of rendering would be:
page.s.coffee
:- Executes its own SSR logic and prepares initial content.
./layout.s.coffee
:- Imports
page.s.coffee
and receives its content. - Executes its
render
function, potentially modifying or enhancing the content frompage.s.coffee
. - Passes the updated content to
../layout.s.coffee
.
- Imports
../layout.s.coffee
:- Imports
./layout.s.coffee
and receives its content. - Executes its
render
function, further modifying or enhancing the content. - Produces the final HTML output, including all layout elements and the content from
page.s.coffee
.
- Imports
Svr::router(options)
Creates a new router for defining custom routes. More about the routers in here: IttyRouter
Parameters:
options
: An object with the following properties:id
: (Optional) The ID of the router.type
: (Optional) Type of the router:"auto"
,"normal"
, or"default"
.- Other properties: (Optional) Additional properties for router configuration.
Returns:
- A
ModuleServeRouter
instance for managing routes.
Example:
router = Svr::router
id: 'main'
type: 'normal'
router.get '/api/hello', (req) ->
Svr::text 'Hello API'
server = Svr::create routers: [router], fetch: 'router'
# or you can use:
# server = Svr::create fetch: router.fetch
server
.port 3000
.listen
Built-in Helpers
Svr::text(string, options)
Returns a plain text response.
Example:
Svr::create fetch: () -> Svr::text "Plain text response"
Svr::json(object, options)
Returns a JSON response.
Example:
Svr::create fetch: () -> Svr::json key: "value"
Svr::html(string, options)
Returns an HTML response.
Example:
Svr::create fetch: () -> Svr::html "<h1>Hello, World!</h1>"
Svr::status(code, options)
Sets the HTTP status code.
Example:
Svr::create fetch: () -> Svr::status 404, "Not Found"
Full example
A simple example to showcase the usage of serve
Svr = imp 'serve'
router
.get '/', -> Svr::text 'hello'
.get '/html', -> Svr::html "<b>Hello!</b>"
.get '/json', -> Svr::json { myJson: "value" }
.post '/json', (req) ->
print req
Svr::json { done: true }
svr = Svr::create fetch: router.fetch
svr.port 3000
.listen
.log 'Listening on $port'