| title | Send a JSON Response | |||||
|---|---|---|---|---|---|---|
| id | send-json-response | |||||
| skillLevel | beginner | |||||
| applicationPatternId | building-apis | |||||
| summary | Create and send a structured JSON response with the correct headers and status code. | |||||
| tags |
|
|||||
| rule |
|
|||||
| author | PaulJPhilp | |||||
| related |
|
|||||
| lessonOrder | 4 |
To return a JavaScript object or value as a JSON response, use the Http.response.json(data) constructor.
APIs predominantly communicate using JSON. The Http module provides a dedicated Http.response.json helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.
Using Http.response.json is superior because:
- Automatic Serialization: It safely handles the
JSON.stringifyoperation for you, including handling potential circular references or other serialization errors. - Correct Headers: It automatically sets the
Content-Type: application/json; charset=utf-8header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs. - Simplicity and Readability: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP.
- Composability: It creates a standard
Http.responseobject that works seamlessly with all other parts of the EffectHttpmodule.
This example defines a route that fetches a user object and returns it as a JSON response. The Http.response.json function handles all the necessary serialization and header configuration.
import { Effect, Context, Duration, Layer } from "effect";
import { NodeContext, NodeHttpServer } from "@effect/platform-node";
import { createServer } from "node:http";
const PORT = 3459; // Changed port to avoid conflicts
// Define HTTP Server service
class JsonServer extends Effect.Service<JsonServer>()("JsonServer", {
sync: () => ({
handleRequest: () =>
Effect.succeed({
status: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "Hello, JSON!",
timestamp: new Date().toISOString(),
}),
}),
}),
}) {}
// Create and run the server
const program = Effect.gen(function* () {
const jsonServer = yield* JsonServer;
// Create and start HTTP server
const server = createServer((req, res) => {
const requestHandler = Effect.gen(function* () {
try {
const response = yield* jsonServer.handleRequest();
res.writeHead(response.status, response.headers);
res.end(response.body);
// Log the response for demonstration
yield* Effect.logInfo(`Sent JSON response: ${response.body}`);
} catch (error: any) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal Server Error" }));
yield* Effect.logError(`Request error: ${error.message}`);
}
});
Effect.runPromise(requestHandler);
});
// Start server with error handling
yield* Effect.async<void, Error>((resume) => {
server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));
} else {
resume(Effect.fail(error));
}
});
server.listen(PORT, () => {
resume(Effect.succeed(void 0));
});
});
yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);
yield* Effect.logInfo("Try: curl http://localhost:3459");
// Run for a short time to demonstrate
yield* Effect.sleep(Duration.seconds(3));
// Shutdown gracefully
yield* Effect.sync(() => server.close());
yield* Effect.logInfo("Server shutdown complete");
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Server error: ${error.message}`);
return error;
})
),
// Merge layers and provide them in a single call to ensure proper lifecycle management
Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))
);
// Run the program
// Use Effect.runFork for server applications that shouldn't resolve the promise
Effect.runPromise(
program.pipe(
// Ensure the Effect has no remaining context requirements for runPromise
Effect.map(() => undefined)
)
);The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.
import { Effect } from "effect";
import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";
const getUserRoute = Http.router.get(
"/users/1",
Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe(
Effect.flatMap((user) => {
// Manually serialize the object to a JSON string.
const jsonString = JSON.stringify(user);
// Create a text response with the string.
const response = Http.response.text(jsonString);
// Manually set the Content-Type header.
return Effect.succeed(
Http.response.setHeader(
response,
"Content-Type",
"application/json; charset=utf-8"
)
);
})
)
);
const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));
const program = Http.server
.serve(app)
.pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));
NodeRuntime.runMain(program);This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the setHeader call, many clients will fail to parse the response correctly. The Http.response.json helper eliminates this entire class of potential bugs.