// MiniExpress.java
// A tiny Express-like framework in pure Java (no external dependencies)
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
public class MiniExpress {
/* ========================== */
/* TYPES */
/* ========================== */
@FunctionalInterface
public interface Next {
void next() throws IOException;
}
@FunctionalInterface
public interface Middleware {
void handle(Request req, Response res, Next next) throws IOException;
}
@FunctionalInterface
public interface Handler {
void handle(Request req, Response res, Next next) throws IOException;
}
/* ========================== */
/* REQUEST */
/* ========================== */
public static class Request {
private final HttpExchange ex;
private String cachedBody = null;
private final Map<String, Object> attributes = new HashMap<>();
public Request(HttpExchange ex) {
this.ex = ex;
}
public String method() {
return ex.getRequestMethod();
}
public String path() {
return ex.getRequestURI().getPath();
}
public Headers headers() {
return ex.getRequestHeaders();
}
public Map<String, Object> attributes() {
return attributes;
}
public Map<String, String> query() {
return parseQuery(ex.getRequestURI().getRawQuery());
}
public synchronized String body() throws IOException {
if (cachedBody != null) return cachedBody;
InputStream is = ex.getRequestBody();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[2048];
int len;
while ((len = is.read(buf)) != -1) {
baos.write(buf, 0, len);
}
cachedBody = baos.toString(StandardCharsets.UTF_8);
return cachedBody;
}
private Map<String, String> parseQuery(String q) {
Map<String, String> map = new HashMap<>();
if (q == null || q.isEmpty()) return map;
for (String part : q.split("&")) {
int idx = part.indexOf('=');
try {
if (idx >= 0) {
String k = URLDecoder.decode(part.substring(0, idx), "UTF-8");
String v = URLDecoder.decode(part.substring(idx + 1), "UTF-8");
map.put(k, v);
} else {
map.put(URLDecoder.decode(part, "UTF-8"), "");
}
} catch (Exception ignored) {}
}
return map;
}
}
/* ========================== */
/* RESPONSE */
/* ========================== */
public static class Response {
private final HttpExchange ex;
private int status = 200;
public Response(HttpExchange ex) {
this.ex = ex;
}
public void status(int code) {
this.status = code;
}
public Headers headers() {
return ex.getResponseHeaders();
}
public void set(String key, String value) {
headers().set(key, value);
}
public void send(String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
if (!headers().containsKey("Content-Type")) {
headers().set("Content-Type", "text/plain; charset=utf-8");
}
ex.sendResponseHeaders(status, bytes.length);
try (OutputStream os = ex.getResponseBody()) {
os.write(bytes);
}
}
public void json(String json) throws IOException {
set("Content-Type", "application/json; charset=utf-8");
send(json);
}
}
/* ========================== */
/* ROUTER */
/* ========================== */
private static class Route {
final String method;
final String path;
final Handler handler;
public Route(String method, String path, Handler handler) {
this.method = method.toUpperCase();
this.path = path;
this.handler = handler;
}
}
/* ========================== */
/* APP */
/* ========================== */
public static class App {
private final List<Middleware> middlewares = new ArrayList<>();
private final List<Route> routes = new ArrayList<>();
private HttpServer server;
// Inside App class:
public App staticFiles(String directory) {
File root = new File(directory);
this.use((req, res, next) -> {
String path = req.path();
if (path.equals("/")) {
next.next(); // allow real route "/" to work
return;
}
File file = new File(root, path);
if (!file.exists() || file.isDirectory()) {
next.next(); // continue to routes
return;
}
// Detect content-type
String mime = "text/plain";
if (path.endsWith(".html")) mime = "text/html";
if (path.endsWith(".css")) mime = "text/css";
if (path.endsWith(".js")) mime = "application/javascript";
if (path.endsWith(".png")) mime = "image/png";
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) mime = "image/jpeg";
if (path.endsWith(".gif")) mime = "image/gif";
res.set("Content-Type", mime);
byte[] bytes = java.nio.file.Files.readAllBytes(file.toPath());
res.status(200);
res.ex.sendResponseHeaders(200, bytes.length);
OutputStream os = res.ex.getResponseBody();
os.write(bytes);
os.close();
});
return this;
}
public App use(Middleware m) {
middlewares.add(m);
return this;
}
public App get(String path, Handler h) {
routes.add(new Route("GET", path, h));
return this;
}
public App post(String path, Handler h) {
routes.add(new Route("POST", path, h));
return this;
}
private void handle(HttpExchange ex) throws IOException {
Request req = new Request(ex);
Response res = new Response(ex);
List<Middleware> chain = new ArrayList<>(middlewares);
// find the matching route
Route matched = null;
for (Route r : routes) {
if (r.method.equals(ex.getRequestMethod()) && r.path.equals(ex.getRequestURI().getPath())) {
matched = r;
break;
}
}
if (matched != null) {
Route finalRoute = matched;
chain.add((rq, rs, next) -> finalRoute.handler.handle(rq, rs, next));
} else {
chain.add((rq, rs, next) -> {
rs.status(404);
rs.send("Not Found");
});
}
runMiddleware(chain, 0, req, res);
}
private void runMiddleware(List<Middleware> chain, int idx, Request req, Response res) throws IOException {
if (idx >= chain.size()) return;
Middleware m = chain.get(idx);
Next next = () -> runMiddleware(chain, idx + 1, req, res);
m.handle(req, res, next);
}
public void listen(int port) throws IOException {
server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/", this::handle);
server.setExecutor(Executors.newCachedThreadPool());
server.start();
System.out.println("MiniExpress running at: http://localhost:" + port);
}
}
/* ========================== */
/* JSON QUOTER */
/* ========================== */
static class JSONObjectSafe {
static String quote(String s) {
if (s == null) return "\"\"";
return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
}
/* ========================== */
/* DEMO SERVER */
/* ========================== */
public static void main(String[] args) throws Exception {
App app = new App();
// serve static files in /public
app.staticFiles("public");
// Logger
app.use((req, res, next) -> {
System.out.println(req.method() + " " + req.path());
next.next();
});
// Body parser
app.use((req, res, next) -> {
if (req.method().equals("POST")) {
req.attributes().put("body", req.body());
}
next.next();
});
// GET /
app.get("/", (req, res, next) -> {
// serve /public/index.html when user visits "/"
File index = new File("public/index.html");
if (index.exists()) {
String html = new String(java.nio.file.Files.readAllBytes(index.toPath()));
res.set("Content-Type", "text/html");
res.send(html);
} else {
res.send("Hello from MiniExpress!");
}
});
app.listen(80);
}
}